Weatherender
Production-grade weather application with a Flask web interface, a CLI tool, and a high-performance async FastAPI v2 API, built as a portfolio project demonstrating real-world engineering practices, has been developed by Alexey Lyapin.
Live Demo
The application is currently in a fully stable, containerized, and production-grade state. It will remain active and autonomously maintained in the cloud.
Infrastructure Note: Hosted on the Render free tier. To bypass the default 15-minute spin-down restriction, the application is kept active via a dedicated background automated worker (UptimeRobot) targeting the lightweight database-free
/api/pingendpoint every 10 minutes.
Availability and Known Limitations
- Cold Starts: Despite the cron-ping system, occasional "cold starts" (30–50s delays) may still occur due to Render's internal container recycling or rare service interruptions.
- Database Pauses: The production storage operates on a free Supabase instance. If the database receives absolutely no client traffic for 7+ consecutive days, Supabase will automatically pause the project. If this occurs, feel free to open an issue to request a manual wake-up.
Stack in production: Render (app) + Supabase (PostgreSQL) + Upstash (Redis).
Install
Python package (CLI)
pip install weatherender
weatherender
Requires Python 3.13+ and a configured .env (at minimum WEATHER_API_KEY).
Docker image (API)
docker pull ghcr.io/weatherender-foundation/weatherender-api:latest
For the full local stack (web + api + postgres + redis) use Docker Compose below.
Features
- 🌦 Current weather + 3-day forecast via WeatherAPI
- ❄️ Advanced Snow Surface Condition Index(SSCI): a unique algorithmic snow condition and quality detection system (featuring statuses like Dry champagne powder!, Ice crust, Spring slush, Wind slab, etc.). The analysis evaluates diurnal temperature cycles, wind speed, snow density, and 24-hour precipitation metrics.
- 🖥 Web interface (Flask) and CLI tool, sharing a common service/model layer
- 📍 Automatic city detection by IP (with fallback chain: ip-api.com → ipinfo.io)
- 🗄 PostgreSQL persistence via SQLAlchemy + Alembic migrations
- 🧹 Automated DB cleanup: background storage rotation powered by
APScheduler(running weekly with a file-lock mechanism to prevent duplicate worker triggers) to safely stay within DB limits - ✅ Input validation with Marshmallow (sync) and Pydantic v2 (async)
- 🛡️ Resilience & retries (
tenacity): exponential backoff wrapper for upstream WeatherAPI calls (synchronous for v1 Flaskrequests, asynchronous non-blocking for v2 FastAPIhttpx) - 🚦 Rate limiting & protection: granular per-worker rate limiting (
flask-limiterfor v1,slowapifor v2), confirmed experimentally viascripts/check_limit.sh(400 sequential requests, graceful429 Too Many Requestsdegradation beyond the quota) - 🐳 Fully containerized with Docker Compose
- 🐬 Docker image on GHCR:
ghcr.io/weatherender-foundation/weatherender-api - 🐍 Published on PyPI:
pip install weatherender - 🔄 CI/CD via GitHub Actions (build, migrate, health check, image publish)
- 🧪 142+ automated tests (pytest): unit, mocked service, Flask & FastAPIroute, and real PostgreSQL integration tests
- 🔌 JSON REST API (
/api/weatherand/api/v2/weather) with interactive Swagger/OpenAPI docs - ⚡️ Redis caching for WeatherAPI responses (TTL-based, graceful fallback on Redis unavailability)
- 📊 Prometheus metrics endpoint (
/metrics) for observability - ❤️ Readiness health check (
/health) with Docker/Compose integration - 🔒 Security hardening: secure headers (Talisman), request size limits, User-Agent validation
- 📝 Structured JSON logging
- 🚀 Load-tested with k6 (smoke, load, stress, spike) — see
docs/PERFORMANCE.md - ☁️ Production cloud deployment: hosted on Render, integrated with Supabase (PostgreSQL) and Upstash (Redis), featuring an automated heartbeat worker (UptimeRobot) to maintain 24/7 web service availability
Tech Stack
- Backend:
Python 3.13,FastAPI (ASGI core),Flask WSGI via WSGIMiddleware,Uvicorn,Gunicorn (gevent workers),SQLAlchemy (sync/async),Alembic,Marshmallow,Pydantic v2,Flask-Limiter,SlowAPI,Tenacity - Database:
PostgreSQL - Infrastructure & DevOps:
Docker,Docker Compose,GitHub Actions (CI/CD),APScheduler (for db clear) - Testing & Quality:
Pytest,unittest.mock,Codecov,k6 (smoke, load, stress, spike) - API & Docs:
FastAPI Auto Docs (Swagger/ReDoc),apispec,flask-swagger-ui (OpenAPI 3.0) - Observability:
prometheus-flask-exporter,structured JSON logging - Security:
flask-talisman - Caching:
Redis,redis-py
Quick Start (Docker)
Prefer not to run it locally? Try the live demo above.
- Clone the repo and copy the environment template:
git clone https://github.com/Weatherender-foundation/Weatherender.git
cd Weatherender
cp .env.example .env
- Fill in
.env— at minimum you'll need a free API key from weatherapi.com (WEATHER_API_KEY) and aSECRET_KEY:
python -c "import secrets; print(secrets.token_hex(32))"
- Start the stack:
Make sure Docker Desktop is installed
docker compose up -d
docker compose run --rm cli alembic upgrade head
-
Open http://localhost:5001 for the Flask-only process (UI + sync API).
Open http://localhost:8001 for the combined process: FastAPI v2 and the same Flask app mounted via
WSGIMiddleware(this is what production runs). Docs: http://localhost:8001/v2/docs.
Local ports (from .env.example):
| Service | Host port | Serves |
|---|---|---|
web |
5001 |
Gunicorn + Flask only (UI, /api/weather, /health, /apidocs) |
api |
8001 |
Uvicorn + FastAPI + Flask mounted (v2 and v1/UI) |
| PostgreSQL | 5432 |
— |
| Redis | 6379 |
— |
| Test PostgreSQL | 5433 |
— |
Running the CLI
Via Docker Compose:
docker compose run --rm cli weatherender
# or:
docker compose run --rm cli python -m weatherender.CLI.main
Via the PyPI package (after pip install weatherender and a local .env):
weatherender
API
The app exposes a JSON REST API alongside the web UI.
| Endpoint | Method | Description |
|---|---|---|
/api/v2/weather |
GET | High-performance async API v2 with Pydantic validation (rate limited: 25 req/min) |
/api/v2/health |
GET | Async DB health check |
/v2/redoc |
GET | Alternative FastAPI ReDoc documentation |
/v2/docs |
GET | Interactive FastAPI OpenAPI/Swagger documentation |
/api/weather |
GET | Get current weather + forecast for a city (?city=Berlin) |
/api/apispec.json |
GET | Raw OpenAPI 3.0 specification (v1) |
/health |
GET | Readiness check (verifies DB connectivity) |
/metrics |
GET | Prometheus metrics |
/api/ping |
GET | Ping endpoint for uptime monitors (no DB connection) |
/apidocs |
GET | Interactive Swagger/OpenAPI documentation (v1) |
/api/weatherand/api/v2/weatherresponses are cached in Redis for 5 minutes (configurable viaREDIS_TTL). If Redis is unavailable, the app falls back to fetching fresh data from WeatherAPI directly.
Full reference: docs/API.md.
Performance & Caching Strategy
To ensure high performance and minimize reliance on external services, the application implements a multi-layered caching and optimization architecture:
- Redis Integration: Weather data fetched from WeatherAPI is cached in an Upstash Redis instance with a 5-minute TTL (
REDIS_TTL). Subsequent requests for the same city are served instantly from the cache, saving external API quotas. - Graceful Degradation: If the Redis instance becomes temporarily unavailable, the application automatically catches the exception and gracefully falls back to direct API fetching without disrupting the user experience.
- Database Efficiency: The automated uptime monitor triggers a lightweight
/api/pingroute that does not open SQLAlchemy sessions or hit the database. This prevents creating redundant connections on the free Supabase tier, keeping the connection pool clean.
Performance Testing
The application has been load, stress, and spike tested with k6 — both against the live Render deployment and locally via Docker Compose. Full methodology and results, including a real bottleneck investigation (sync → gevent Gunicorn workers), are documented in docs/PERFORMANCE.md. Scripts live in load_tests/.
Running Load Tests Locally
k6 must be installed locally (it isn't bundled as a Docker Compose service). Smoke and load tests target the live Render deployment directly; stress and spike tests require the local stack running first.
1. Smoke Test (Render — verify the live deployment is alive and stable)
k6 run load_tests/smoke.js
2. Load Test (Render — realistic traffic across all endpoints)
k6 run load_tests/load.js
3. Stress Test (local — find the system's breaking point) — running locally
docker compose up -d
k6 run load_tests/stress.js
4. Spike Test (local — validate resilience against sudden traffic bursts) — running locally
docker compose up -d
k6 run load_tests/spike.js
Further Documentation
docs/ARCHITECTURE.md— component responsibilities, request/data flow, infrastructuredocs/DEPLOYMENT.md— how the production deployment (Render + Supabase + Upstash) is set up and reproduceddocs/API.md— full API reference (endpoints, params, error formats, rate limiting)docs/PERFORMANCE.md— load-testing methodology and resultsdocs/CHANGELOG.md— project history by date/theme
Running Tests
Tests require a dedicated PostgreSQL test container (kept separate from the dev/prod database):
docker compose up -d weather_test_db
DATABASE_URL="postgresql://test_user:test_password@localhost:5433/test_weather_db" alembic upgrade head
pytest -v
Via Compose (same path as CI):
docker compose run --rm -v "$PWD":/app cli pytest tests/ --cov=. --cov-report=xml --asyncio-mode=auto
Note: test_cache.py mocks the Redis client directly and does not require a running Redis instance.
Engineering Standards & Git Flow
This repository strictly adheres to professional enterprise software development practices:
- Feature Branching: Every bug fix, optimization, and component expansion is developed in isolated branches (
feature/*,fix/*) to ensure themainbranch remains stable and deployable at all times. - True Merging: The project utilizes explicit merge commits via the
--no-ff(no fast-forward) strategy to preserve a rich, readable, and non-linear history of architectural iterations. - Conventional Commits: Commit messages are standardized using semantic prefixes (
feat:,fix:,docs:,refactor:,tests:) to provide transparency in project growth and documentation maintenance. - Issue-Driven Post-Mortems: Real infrastructure incidents and complex bug fixes are documented in the repository's closed Issues as production post-mortems, tracking root cause analysis and technical resolutions.
- Automated CI/CD & Safe Deployment: Completely automated integration via GitHub Actions. The workflow forces strict validation layers (Ruff linting, Mypy type-checking, and full Pytest execution) before triggering a production release. Automated deployment via Render webhooks is strictly gated and will automatically block if any unit test or linting check fails.
Roadmap & Future Enhancements
The next major architectural evolution of Weatherender is fully planned:
- Dynamic Radar Maps: Integrate interactive precipitation radar and dynamic weather maps using GIS/Leaflet tools to visualize snow and rain fronts
- Asynchronous API v2 (FastAPI Integration): High-performance
api/v2microservice using FastAPI (asyncio,asyncpg,httpx,Pydantic v2) mounted alongside Flask viaWSGIMiddleware - Python package & GHCR image: Installable
weatherenderon PyPI and published API image on GitHub Container Registry - User Authentication & Custom Alerts: Implement secure JWT or session-based user authentication via Supabase Auth, allowing skiers to save favorite resorts and customize automated notification limits
Project Structure & Architecture
The installable package lives under src/weatherender/:
src/weatherender/
├── WEB/ # Flask UI + v1 JSON API (Compose `web`, port 5001 — Flask only)
├── API/ # FastAPI v2 + mounted Flask (Compose `api` / Render / GHCR, port 8001)
├── CLI/ # console script: weatherender
├── config.py
├── models.py
├── services.py
├── cache.py
├── schemas.py
└── snow.py
Locally, Compose runs web (port 5001, Flask only) and api (port 8001, FastAPI + Flask via WSGIMiddleware) as separate containers. Production and GHCR use only the API image, so one uvicorn process serves both stacks — the same shape as local :8001.
Full breakdown, component responsibilities, and request/data flow diagrams: docs/ARCHITECTURE.md.
Engineering Challenges & Bug Investigations
A full technical deep-dive into blockers, edge cases, and architectural updates can be tracked in closed Project Issues. Key production-level engineering challenges solved during development include:
1. Database Pollution Mitigation & HTTP HEAD Short-Circuiting (Issue #9)
- The Incident (Post-Mortem): Continuous 10-minute uptime checks from UptimeRobot targeting the root route (
/) were heavily polluting the operational logs and telemetry within the Supabase PostgreSQL storage layer, triggering unintended database writes and accumulating thousands of empty automated tracking rows. - Root Cause Analysis: The external monitor was explicitly configured to use the
HTTP HEADmethod. However, because Flask by default implicitly converts unhandledHEADrequests toGETon routes without explicit declaration, the request bypassed customUser-Agentstring filtering layers. This initiated a redundantSessionLocal()DB connection and triggered execution of an unpredictable insert query lifecycle on every health probe. - Engineering Solution & Shield:
- Updated the internal
/api/pingroute configuration withinapi_routes.pyto explicitly support bothGETandHEADmethods to handle direct infrastructure probes natively. - Overhauled the core index (
/) route decorator inapp.pyby introducing explicitHEADsupport (methods=["GET", "POST", "HEAD"]). - Placed a high-priority, zero-cost early return check (
if request.method == "HEAD": return "") at the very first line of execution.
- Updated the internal
- Results: Database pollution was successfully brought down to 0%, server resources were completely preserved, and database sessions are now never initialized for non-human monitoring traffic.
2. Flask-Limiter Blueprint Registry Mismatch & Circular Dependency Resolution (Issue #10)
- The Incident: During pre-merge manual load testing using a custom
check_limit.shscript (firing 400 sequential requests), the automated uptime monitor health checks against/api/pingconsistently failed with429 Too Many Requests. The endpoint kept throttling traffic even though an explicitlimiter.exempt(ping)rule was active inapp.py. - Root Cause Analysis: A deep-dive revealed a lifecycle mismatch in how
flask-limiter==4.1.1registers routes. When applying a blueprint-wide limit (api_bp), the@limiter.exemptdecorator failed because it looked up the bare function name, whereas Flask resolves the request's endpoint at dispatch time using the prefixed name (api.ping). Furthermore, attempting to apply decorators directly insideapi_routes.pyintroduced immediate circular imports betweenapp.pyand the routing module. - Engineering Solution:
- Broke the circular dependency chain by decoupling the
Limiterinstance instantiation, moving it into an uninitialized state inside a newly designed infrastructure layer:src/weatherender/WEB/extensions.py. - Late-bound the engine during the application factory setup in
app.pyvialimiter.init_app(app). - Dropped the fragile blueprint-wide mapping logic entirely. Instead, explicitly declared per-route limits using
@limiter.limit("25 per minute")directly on the protected production endpoints (get_weatherandget_apispec), leaving the critical/api/pingroute cleanly undecorated and inherently immune to rate-limiting blocks.
- Broke the circular dependency chain by decoupling the
- Results: Re-running the 400-request load test confirmed 100% success on
/api/ping(returning pure200 OKmetrics alongside expected gevent network socket resets), while public endpoints correctly isolated and blocked aggressive traffic bursts.
About the Author
It's been developing since I was 13 years old.
I'm an active alpine skier (currently holding the 2nd adult sports rank and training for the 1st) and a full-time student at Gymnasium 1514. Due to intensive academic tracking and a demanding winter training schedule on the ski slopes, my development velocity temporarily transitions into a maintenance phase during the winter season — full-scale feature development resumes during the next summer cycle.
I also write about the engineering side of this project on my Habr profile.
License
This project is licensed under the SSCI Custom License v1.2.
🛡️ License: Non-Commercial Use Only You may download and use this package for personal, educational, or non-commercial purposes. Commercial use (selling, SaaS, monetization) is strictly prohibited without written permission. Full license text: SSCI Custom License v1.2
For commercial licensing inquiries, contact:
Metadata
Release files for weatherender 2.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| weatherender-2.0.1.tar.gz | 52.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| weatherender-2.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 91.6 kB
Release files / weatherender-2.0.1.tar.gz
| Download URL | weatherender-2.0.1.tar.gz |
|---|---|
| Size | 52.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
233b147517f1b3f060e0c0e7775a6f2a7456a84b53cf5e452c7be28f739e1a80
|
|
BLAKE2b-256 checksum How to use checksums |
1290b04618f20d8d0e7ad45f6327b2191cbdef2584dded5bb0992d9a24467cd3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency logRelease files / weatherender-2.0.1-py3-none-any.whl
| Download URL | weatherender-2.0.1-py3-none-any.whl |
|---|---|
| Size | 39.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
30aea99173e637610ff6d202f37779041677d53172380775e37653307f964ac5
|
|
BLAKE2b-256 checksum How to use checksums |
e01414da098546afad8111d5297c16d02f9b54117c719798c5f633de0b075958
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency log