# wttr.in Architecture Documentation ## System Overview wttr.in is a console-oriented weather forecast service with a hybrid Python/Go architecture: - **Go proxy layer** (cmd/): LRU caching proxy with prefetching - **Python backend** (lib/, bin/): Weather data fetching, formatting, rendering - **Static assets** (share/): Translations, templates, emoji, help files ## Request Flow ``` Client Request ↓ Go Proxy (port 8082) - LRU cache + prefetch ↓ (cache miss) Python Backend (port 8002) - Flask/gevent ↓ Location Resolution (GeoIP/IP2Location/IPInfo) ↓ Weather API (met.no or WorldWeatherOnline) ↓ Format & Render (ANSI/HTML/PNG/JSON/Prometheus) ↓ Response (cached with TTL 1000-2000s) ``` ## Component Breakdown ### 1. Go Proxy Layer (cmd/) **Files:** - `cmd/srv.go` - Main HTTP server (port 8082) - `cmd/processRequest.go` - Request processing & caching logic - `cmd/peakHandling.go` - Peak time prefetching (cron-based) **Responsibilities:** - LRU cache (12,800 entries, 1000-1500s TTL) - Cache key: `UserAgent:Host+URI:ClientIP:AcceptLanguage` - Prefetch popular requests at :24 and :54 past the hour - Forward cache misses to Python backend (127.0.0.1:9002) - Handle concurrent requests (InProgress flag prevents thundering herd) **Key Logic:** - `dontCache()`: Skip caching for cyclic requests (location contains `:`) - `getCacheDigest()`: Generate cache key from request metadata - `processRequest()`: Main request handler with cache-aside pattern - `savePeakRequest()`: Record requests at :30 and :00 for prefetching ### 2. Python Backend (bin/, lib/) #### Entry Points (bin/) **bin/srv.py** - Main Flask application - Listens on port 8002 (configurable via WTTRIN_SRV_PORT) - Routes: `/`, `/`, `/files/`, `/favicon.ico` - Uses gevent WSGI server for async I/O - Delegates to `wttr_srv.wttr()` for all weather requests **bin/proxy.py** - Weather API proxy (separate service) - Caches weather API responses - Transforms met.no/WWO data to standard JSON - Test mode support (WTTRIN_TEST env var) - Handles translations for weather conditions **bin/geo-proxy.py** - Geolocation service proxy - Not examined in detail (separate microservice) #### Core Logic (lib/) **lib/wttr_srv.py** - Main request handler - `wttr(location, request)` - Entry point for all weather queries - `parse_request()` - Parse location, language, format from request - `_response()` - Generate response (checks cache, calls renderers) - Rate limiting (300/min, 3600/hour, 24*3600/day per IP) - ThreadPool (25 workers) for PNG rendering - Two-phase processing: fast path (cache/static) then full path **lib/parse_query.py** - Query string parsing - Parse single-letter options: `n`=narrow, `m`=metric, `u`=imperial, `T`=no-terminal, etc. - Parse PNG filenames: `City_200x_lang=ru.png` → structured dict - Serialize/deserialize query state (base64+zlib for short URLs) - Metric vs imperial logic (US IPs default to imperial) **lib/location.py** - Location resolution - `location_processing()` - Main entry point - IP → Location: GeoIP2 (MaxMind), IP2Location API, IPInfo API - Location normalization (lowercase, strip special chars) - Geolocator service (localhost:8004) for GPS coordinates - IATA airport code support - Alias resolution (share/aliases file) - Blacklist checking (share/blacklist file) - Hemisphere detection for moon phases - Special prefixes: - `~` = search term (use geolocator) - `@` = domain name (resolve to IP first) - No prefix = exact location name **lib/globals.py** - Configuration - Environment variables: WTTR_MYDIR, WTTR_GEOLITE, WTTR_WEGO, etc. - File paths: cache dirs, static files, translations - API keys: IP2Location, IPInfo, WorldWeatherOnline - Constants: NOT_FOUND_LOCATION, PLAIN_TEXT_AGENTS, QUERY_LIMITS - IP location order: geoip → ip2location → ipinfo **lib/cache.py** - LRU cache (Python side) - In-memory LRU (10,000 entries, pylru) - File cache for large responses (>80 bytes) - TTL: 1000-2000s (randomized) - Cache key: `UserAgent:QueryString:ClientIP:Lang` - Dynamic timestamp replacement: `%{{NOW(timezone)}}` **lib/limits.py** - Rate limiting - Per-IP query limits (minute/hour/day buckets) - Whitelist support - Returns 429 on limit exceeded #### View Renderers (lib/view/) **lib/view/wttr.py** - Main weather view - Calls `wego` (Go binary) for weather rendering - Passes flags: -inverse, -wind_in_ms, -narrow, -lang, -imperial - Post-processes output (location name, formatting) - Converts to HTML if needed **lib/view/line.py** - One-line format - Formats: 1, 2, 3, 4, or custom with % notation - Custom format codes: %c=condition, %t=temp, %h=humidity, %w=wind, etc. - Supports multiple locations (`:` separated) **lib/view/v2.py** - Data-rich v2 format - Experimental format with more detail - Moon phase, astronomical times, temperature graphs - Terminal-only, English-only **lib/view/moon.py** - Moon phase view - Uses `pyphoon-lolcat` for rendering - Supports date selection: `Moon@2016-12-25` **lib/view/prometheus.py** - Prometheus metrics - Exports weather data as Prometheus metrics - Format: `p1` #### Formatters (lib/fmt/) **lib/fmt/png.py** - PNG rendering - Converts ANSI terminal output to PNG images - Uses pyte (terminal emulator) + PIL - Transparency support - Font rendering **lib/fmt/unicodedata2.py** - Unicode handling - Character width calculations for terminal rendering #### Other Modules (lib/) **lib/translations.py** - i18n support - 54 languages supported - Weather condition translations - Help file translations (share/translations/) - Language detection from Accept-Language header **lib/constants.py** - Weather constants - Weather codes (WWO API) - Condition mappings - Emoji mappings **lib/buttons.py** - HTML UI elements - Add interactive buttons to HTML output **lib/fields.py** - Data field extraction - Parse weather API responses **lib/weather_data.py** - Weather data structures **lib/airports.py** - IATA code handling **lib/metno.py** - met.no API client - Norwegian Meteorological Institute API - Transforms to standard JSON format ### 3. Static Assets (share/) **share/translations/** - 54 language files - Format: `{lang}.txt` (weather conditions) - Format: `{lang}-help.txt` (help pages) **share/emoji/** - Weather emoji PNGs - Used for PNG rendering **share/static/** - Web assets - favicon.ico - style.css - example images **share/templates/** - Jinja2 templates - index.html (HTML output wrapper) **share/** - Data files - `aliases` - Location aliases (from:to format) - `blacklist` - Blocked locations - `list-of-iata-codes.txt` - Airport codes - `help.txt` - English help - `bash-function.txt` - Shell integration - `translation.txt` - Translation info page ## API Endpoints ### Weather Queries - `GET /` - Weather for IP-based location - `GET /{location}` - Weather for specific location - `GET /{location}.png` - PNG image output - `GET /{location}?{options}` - Weather with options ### Special Pages - `GET /:help` - Help page - `GET /:bash.function` - Shell function - `GET /:translation` - Translation info - `GET /:iterm2` - iTerm2 integration ### Static Files - `GET /files/{path}` - Static assets - `GET /favicon.ico` - Favicon ## Query Parameters ### Single-letter Options (combined in query string) - `A` - Force ANSI output - `n` - Narrow output - `m` - Metric units - `M` - m/s for wind speed - `u` - Imperial units - `I` - Inverted colors - `t` - Transparency (PNG) - `T` - No terminal sequences - `p` - Padding - `0-3` - Number of days - `q` - No caption - `Q` - No city name - `F` - No follow line ### Named Parameters - `lang={code}` - Language override - `format={fmt}` - Output format (1-4, v2, j1, p1, custom) - `view={view}` - View type (alias for format) - `period={sec}` - Update interval for cyclic locations ### PNG Filename Format `{location}_{width}x{height}_{options}_lang={lang}.png` Example: `London_200x_t_lang=ru.png` ## Output Formats 1. **ANSI** - Terminal with colors/formatting 2. **Plain text** - No ANSI codes (T option) 3. **HTML** - Web browser output 4. **PNG** - Image file 5. **JSON** (j1) - Machine-readable data 6. **Prometheus** (p1) - Metrics format 7. **One-line** (1-4) - Compact formats 8. **v2** - Data-rich experimental format ## External Dependencies ### Weather APIs - **met.no** (Norwegian Meteorological Institute) - Primary, free - **WorldWeatherOnline** - Fallback, requires API key ### Geolocation - **GeoLite2** (MaxMind) - Free GeoIP database (required) - **IP2Location** - Commercial API (optional, needs key) - **IPInfo** - Commercial API (optional, needs key) - **Geolocator service** - localhost:8004 (GPS coordinates) ### External Binaries - **wego** (we-lang) - Go weather rendering binary - **pyphoon-lolcat** - Moon phase rendering ### Python Libraries - Flask - Web framework - gevent - Async I/O - geoip2 - GeoIP lookups - geopy - Geocoding - requests - HTTP client - PIL - Image processing - pyte - Terminal emulator - pytz - Timezone handling - pylru - LRU cache ### Go Libraries - github.com/hashicorp/golang-lru - LRU cache - github.com/robfig/cron - Cron scheduler ## Configuration ### Environment Variables - `WTTR_MYDIR` - Installation directory - `WTTR_GEOLITE` - Path to GeoLite2-City.mmdb - `WTTR_WEGO` - Path to wego binary - `WTTR_LISTEN_HOST` - Bind address (default: "") - `WTTR_LISTEN_PORT` - Port (default: 8002) - `WTTR_USER_AGENT` - Custom user agent - `WTTR_IPLOCATION_ORDER` - IP location method order - `WTTRIN_SRV_PORT` - Override listen port - `WTTRIN_TEST` - Enable test mode ### API Key Files - `~/.wwo.key` - WorldWeatherOnline API key - `~/.ip2location.key` - IP2Location API key - `~/.ipinfo.key` - IPInfo token - `~/.wegorc` - Wego configuration (JSON) ### Data Directories - `/wttr.in/cache/ip2l/` - IP location cache - `/wttr.in/cache/png/` - PNG cache - `/wttr.in/cache/lru/` - LRU file cache - `/wttr.in/cache/proxy-wwo/` - Weather API cache - `/wttr.in/log/` - Log files ## Caching Strategy ### Three-tier Cache 1. **Go LRU** (12,800 entries, 1000-1500s TTL) - In-memory, fastest - Full HTTP responses - Shared across all requests 2. **Python LRU** (10,000 entries, 1000-2000s TTL) - In-memory for small responses (<80 bytes) - File-backed for large responses - Per-process cache 3. **File Cache** - IP location cache (persistent) - Weather API cache (persistent) - PNG cache (persistent) ### Cache Keys - Go: `UserAgent:Host+URI:ClientIP:AcceptLanguage` - Python: `UserAgent:QueryString:ClientIP:Lang` ### Cache Invalidation - TTL-based expiration (no manual invalidation) - Randomized TTL prevents thundering herd - Non-cacheable: cyclic requests (location contains `:`) ## Prefetching - Cron jobs at :24 and :54 past the hour - Records popular requests at :30 and :00 - Spreads prefetch over 5 minutes (300s) - Prevents cache expiry during peak times ## Rate Limiting - Per-IP limits: 300/min, 3600/hour, 24*3600/day - Whitelist support (MY_EXTERNAL_IP) - Returns HTTP 429 on limit exceeded - Implemented in Python layer only ## Error Handling - Location not found → "not found" location (fallback weather) - API errors → 503 Service Unavailable - Malformed requests → 500 Internal Server Error (HTML) or error message (text) - Blocked locations → 403 Forbidden - Rate limit → 429 Too Many Requests ## Logging - Main log: `/wttr.in/log/main.log` - Debug log: `/tmp/wttr.in-debug.log` - Go proxy logs to stdout ## Testing - No unit tests - Integration test: `test/query.sh` - Makes HTTP requests to running server - Compares SHA1 hashes of responses - Test data in `test/test-data/signatures` - CI: flake8 linting only (no actual tests run) ## Known Issues & Limitations 1. No unit test coverage 2. v2 format is experimental (terminal-only, English-only) 3. Moon phase Unicode ambiguity (hemisphere-dependent) 4. Hardcoded IP whitelist (MY_EXTERNAL_IP) 5. Multiple cache layers with different keys 6. Mixed Python/Go codebase 7. External binary dependencies (wego, pyphoon) 8. Requires external geolocator service (port 8004) 9. File cache grows unbounded 10. No cache warming on startup ## Performance Characteristics - Go proxy handles ~12,800 cached requests in memory - Python backend spawns 25 threads for PNG rendering - Gevent provides async I/O for Python - Prefetching reduces latency during peak times - File cache avoids memory pressure for large responses - Rate limiting prevents abuse ## Security Considerations - IP-based rate limiting - Location blacklist - No authentication required - User-provided location names passed to external APIs - File cache uses MD5 hashes (not cryptographic) - No input sanitization for location names - Trusts X-Forwarded-For header