Skip to content

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

weather-cli 🌤️

A weather CLI built as an architecture exercise: fetch data from an API, cache it locally, and render it nicely in the terminal — but built so each piece (data source, cache, presentation, AI) can be swapped without touching the others. Defaults to the Open-Meteo API, which is free and needs no API key, with OpenWeatherMap available as a drop-in alternative.

Table of Contents

🏗️ Architecture

This is a ports & adapters (hexagonal) design: three Protocols in ports.pyWeatherProvider, WeatherCache, AIProvider — describe every external dependency the app has, and everything outside adapters/ depends on those Protocols, never on a concrete class. main.py doesn't know if weather data came from the cache or the network, or which vendor served it. WeatherRepository doesn't know which HTTP client fetched it. ask doesn't know whether AI is even configured. Each of those is one decision, made in exactly one place — a factory function in each adapter package's __init__.py — so swapping any of them out later is a local change instead of a rewrite.

weather_cli/
├── main.py                          # CLI (Typer) — orchestrates, no business logic
├── config.py                        # the only place that reads environment variables
├── ports.py                         # WeatherProvider, WeatherCache, AIProvider protocols
├── exceptions.py                    # domain-specific exception hierarchy
├── domain/
│   └── models.py                    # WeatherData, Location, WeatherCondition (pydantic)
├── adapters/
│   ├── weather/
│   │   ├── open_meteo.py            # Open-Meteo HTTP client (geocoding + forecast)
│   │   ├── open_weather_map.py      # OpenWeatherMap HTTP client
│   │   └── __init__.py              # build_weather_provider(settings) factory
│   ├── cache/
│   │   ├── sqlite_cache.py          # local SQLite cache with TTL
│   │   └── __init__.py              # build_cache(settings) factory
│   └── ai/
│       ├── litellm_adapter.py       # AIProvider backed by LiteLLM (multi-vendor)
│       ├── null_adapter.py          # Null Object used when AI isn't configured
│       └── __init__.py              # build_ai_provider(settings) factory
├── services/
│   └── weather_repository.py        # orchestrates provider + cache behind one interface
└── presentation/
    ├── ascii_art.py                  # ASCII art per weather condition
    └── renderer.py                   # rich-based terminal output

Data flow: main.pyWeatherRepository → (cache or the configured WeatherProvider) → renderer. WeatherRepository is the one piece that decides whether to serve a cached value or fetch a new one — everything upstream of it just calls .get_current() and doesn't care which. Cache entries are namespaced by provider (f"{provider_name}:{city}"), so switching WEATHER_PROVIDER can never serve a stale reading cached under a different vendor.

✨ Features

  • Current weatherweather now <city>, with an ASCII icon per condition.
  • Local caching — SQLite-backed, TTL-based, so repeated lookups don't hit the API.
  • Force refresh--refresh bypasses the cache for a single call.
  • Cache clearingweather clear-cache wipes the local cache.
  • Two weather providers — Open-Meteo (default, no key) or OpenWeatherMap, picked via WEATHER_PROVIDER.
  • AI Q&Aweather ask <city> "<question>", backed by LiteLLM so any supported vendor (Anthropic, OpenAI, OpenRouter, …) works via a single model string. Falls back to a Null Object with a friendly message when AI_PROVIDER_MODEL isn't configured, instead of crashing.

🚀 Quick Start

git clone <your-repo-url>
cd weather-cli-2
pip install -e ".[dev]"

weather now "Blumenau"
weather now "São Paulo" --refresh
weather clear-cache

No .env is required to run this — config.py falls back to sane defaults for the cache TTL, path, and weather provider. Copy .env.example to .env if you want to override those, switch providers, or enable AI.

💾 Cache

Weather responses are cached locally in SQLite (.cache/weather.db by default) with a configurable TTL (10 minutes by default). Every response tells you whether it came from the cache or live from the API. Override either setting via environment variables (in .env or your shell):

CACHE_TTL_SECONDS=600
CACHE_DB_PATH=.cache/weather.db

🌦️ Weather provider

# "open-meteo" (default, no key needed) or "openweathermap"
WEATHER_PROVIDER=open-meteo

# Required only if WEATHER_PROVIDER=openweathermap
OPENWEATHERMAP_API_KEY=

Choosing a provider that requires a key without setting it raises a ConfigurationError, rendered as a friendly message instead of a crash.

🤖 AI integration (optional)

weather ask "Blumenau" "Do I need an umbrella tomorrow?"

AI is powered by LiteLLM, which normalizes calls to Anthropic, OpenAI, OpenRouter, and other providers behind a single completion() call — so adding a vendor is a config change, not a new adapter class. Set AI_PROVIDER_MODEL in your .env to a LiteLLM model string plus the matching API key:

AI_PROVIDER_MODEL=anthropic/claude-sonnet-4-6
ANTHROPIC_API_KEY=

Leave AI_PROVIDER_MODEL empty and weather ask returns a clear "AI is not configured" message instead of crashing — that's NullAIAdapter doing its job.

🧪 Testing

pip install -e ".[dev]"
pytest

50 tests cover the domain models, both weather adapters (including WMO weather-code mapping and malformed-response handling), the SQLite cache, the repository (cache hit/miss/force-refresh, provider-namespaced keys), the AI adapters (LiteLLM and Null Object), config loading, and end-to-end CLI behavior via Typer's test runner.

🤝 Contributing

Bug reports, questions, and pull requests are welcome — see CONTRIBUTING.md for how to get set up and what to expect.

📄 License

MIT — see LICENSE.

About

A weather CLI built as an architecture exercise: fetch data from an API, cache it locally, and render it nicely in the terminal

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages