#!/usr/bin/env python3 import argparse import sys import requests from datetime import datetime # --------------------------------------------------------------------------- # weather - displays key weather information in just two rows # # Blue Oak Model License 1.0.0 # copyright (c) 2025 640kb.neocities.org # # see: ww (worldweather; multiple locations) # Usage: type "weather -h" # # 22dec25: complete rewrite using OpenStreetMap to resolve locations to # latitude/longitude and Open-Meteo for weather retrieval. # --------------------------------------------------------------------------- COUNTRY_CODES = { "US": "UNITED STATES, USA", "GB": "UNITED KINGDOM", "RU": "RUSSIA", "CN": "CHINA", "FR": "FRANCE", "DE": "GERMANY", "JP": "JAPAN", "IN": "INDIA", "BR": "BRAZIL", "CA": "CANADA", "AU": "AUSTRALIA", "IT": "ITALY", "ES": "SPAIN", "KR": "SOUTH KOREA", "MX": "MEXICO", "ZA": "SOUTH AFRICA", "EG": "EGYPT", "NG": "NIGERIA", "KE": "KENYA", "SA": "SAUDI ARABIA", "AE": "UNITED ARAB EMIRATES", "TR": "TURKEY", "NL": "NETHERLANDS", "SE": "SWEDEN", "NO": "NORWAY", "FI": "FINLAND", "DK": "DENMARK", "PL": "POLAND", "CZ": "CZECH REPUBLIC", "GR": "GREECE", } # --------------------------------------------------------------------------- def print_help(): print("") print(" .---------------------------------------------------------------------------.") print(" | Weather: shows key weather data in a compact format |") print(" | |") print(" | Usage: |") print(" | weather [-f | -c] |") print(" | (-zip ZIP | -city CITY [-state STATE] -country COUNTRY) |") print(" | |") print(" | Flags: |") print(" | -f Use Fahrenheit |") print(" | -c Use Celsius |") print(" | -zip ZIP ZIP / postal code (US-centric; may need -country) |") print(" | -city CITY City name (quote multi-word names) |") print(" | -state STATE State / province / region (see notes below) |") print(" | -country CC 2-letter country code (required with -city) |") print(" | -h, --help Show this help and exit |") print(" | --codes List common 2-letter country codes |") print(" | |") print(" | Examples: |") print(" | weather -f -city Springfield -state MO -country US |") print(" | weather -c -city London -country GB |") print(" | weather -c -city Tver -state \"Tver Oblast\" -country RU |") print(" | weather -f -zip 90210 (-country US) |") print(" | weather -c -city \"Grand Falls\" -state NB -country CA |") print(" |---------------------------------------------------------------------------|") print(" | Notes on duplicate city names: |") print(" | |") print(" | * US / Canada: use 2-letter state or province codes (ex: NY, MB) |") print(" | * Other countries: a full region/province name may be required, using |") print(" | official OpenStreetMap naming (ex: Moscow Oblast, Krasnoyarsk Krai) |") print(" |---------------------------------------------------------------------------|") print(" | Country codes reference: |") print(" | weather --codes |") print(" | https://www.iban.com/country-codes |") print(" |---------------------------------------------------------------------------|") print(" | Note: the -zip flag works best when used with the -country flag |") print(" '---------------------------------------------------------------------------'") print("") # --------------------------------------------------------------------------- def get_coordinates_osm(city, state=None, country=None): url = "https://nominatim.openstreetmap.org/search" query = city if state: query += f", {state}" if country: query += f", {country}" params = { "q": query, "format": "json", "limit": 1, "addressdetails": 1 } try: r = requests.get(url, params=params, timeout=10, headers={"User-Agent": "weather-app"}) r.raise_for_status() except requests.RequestException: return None data = r.json() if not data: return None lat = data[0].get("lat") lon = data[0].get("lon") if lat and lon: return float(lat), float(lon) return None # --------------------------------------------------------------------------- def get_weather(lat, lon, unit): temp_unit = "fahrenheit" if unit == "F" else "celsius" wind_unit = "mph" if unit == "F" else "kmh" url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": lat, "longitude": lon, "current": "temperature_2m,wind_speed_10m,weather_code,relative_humidity_2m", "daily": "temperature_2m_max,temperature_2m_min", "temperature_unit": temp_unit, "wind_speed_unit": wind_unit, "timezone": "auto", } try: r = requests.get(url, params=params, timeout=10) r.raise_for_status() return r.json() except requests.RequestException: return None # --------------------------------------------------------------------------- def weather_status(code): codes = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Drizzle", 55: "Dense drizzle", 56: "Light freezing drizzle", 57: "Dense freezing drizzle", 61: "Slight rain", 63: "Rain", 65: "Heavy rain", 66: "Light freezing rain", 67: "Heavy freezing rain", 71: "Slight snow fall", 73: "Snow fall", 75: "Heavy snow fall", 77: "Snow grains", 80: "Slight rain showers", 81: "Rain showers", 82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail", } return codes.get(code, "Unknown") # --------------------------------------------------------------------------- def main(): if '--codes' in sys.argv: print("\n Common Country Codes for Open-Meteo API:") print(" " + "="*40) for code, names in sorted(COUNTRY_CODES.items()): print(f" {code :3} : {names}") print() sys.exit(0) parser = argparse.ArgumentParser(add_help=False) temp_group = parser.add_mutually_exclusive_group(required=True) temp_group.add_argument("-f", action="store_true", help="Use Fahrenheit") temp_group.add_argument("-c", action="store_true", help="Use Celsius") loc_group = parser.add_mutually_exclusive_group(required=True) loc_group.add_argument("-zip", help="ZIP/postal code") loc_group.add_argument("-city", nargs='+', help="City name (use quotes for multi-word)") parser.add_argument("-state", help="State code (e.g., NY, CA). Optional, mainly for US") parser.add_argument("-country", help="Country code (2-letter ISO, e.g., US, FR). Required for -city") parser.add_argument("-h", "--help", action="store_true") if '-h' in sys.argv or '--help' in sys.argv or len(sys.argv) == 1: print_help() sys.exit(0) args = parser.parse_args() unit = "F" if args.f else "C" temp_symbol = "°F" if unit == "F" else "°C" wind_symbol = "mph" if unit == "F" else "km/h" lat = lon = None location_str = "" if args.zip: country = args.country.upper() if args.country else None coords = get_coordinates_osm(args.zip, None, country) if not coords: print(f"Error: Could not find ZIP code '{args.zip}'") sys.exit(1) lat, lon = coords location_str = f"ZIP: {args.zip}" + (f", {country}" if country else "") elif args.city: if not args.country: print("Error: -country is required with -city") sys.exit(1) city = " ".join(args.city) state = args.state.upper() if args.state else None country = args.country.upper() coords = get_coordinates_osm(city, state, country) if not coords: display_loc = f"{city}" if state: display_loc += f", {state}" display_loc += f", {country}" print(f"Error: Could not resolve location '{display_loc}'") sys.exit(1) lat, lon = coords location_str = f"{city}" if state: location_str += f", {state}" location_str += f", {country}" data = get_weather(lat, lon, unit) if not data or "current" not in data: print("Error: Could not fetch weather data") sys.exit(1) current = data["current"] daily = data["daily"] local_dt = datetime.strptime(current["time"], "%Y-%m-%dT%H:%M") day = local_dt.strftime("%a") time_str = local_dt.strftime("%I:%M%p").lstrip("0").lower() print(f"\n{location_str} ({day} {time_str})") print("-" * 80) print(f"Now: {current['temperature_2m']}{temp_symbol} | " f"Wind: {current['wind_speed_10m']}{wind_symbol} | " f"RH: {current['relative_humidity_2m']}% | " f"Status: {weather_status(current['weather_code'])}") print(f"High: {daily['temperature_2m_max'][0]}{temp_symbol} | " f"Low: {daily['temperature_2m_min'][0]}{temp_symbol}") print("-" * 80) # --------------------------------------------------------------------------- if __name__ == "__main__": main()