ezhik.

WeatherNow

Open source
GitHub ↗ Скачать ZIP
weathernow.py8.5 KB · pythonraw
#!/usr/bin/env python3
"""WeatherNow — current weather and forecast in your terminal."""

import argparse
import json
import sys
import urllib.request

class C:
    RED     = "\033[91m"
    GREEN   = "\033[92m"
    YELLOW  = "\033[93m"
    BLUE    = "\033[94m"
    CYAN    = "\033[96m"
    WHITE   = "\033[97m"
    GRAY    = "\033[90m"
    BOLD    = "\033[1m"
    DIM     = "\033[2m"
    RESET   = "\033[0m"


BANNER = f"""
{C.CYAN}{C.BOLD}  ██╗    ██╗███████╗ █████╗ ████████╗██╗  ██╗███████╗██████╗
  ██║    ██║██╔════╝██╔══██╗╚══██╔══╝██║  ██║██╔════╝██╔══██╗
  ██║ █╗ ██║█████╗  ███████║   ██║   ███████║█████╗  ██████╔╝
  ██║███╗██║██╔══╝  ██╔══██║   ██║   ██╔══██║██╔══╝  ██╔══██╗
  ╚███╔███╔╝███████╗██║  ██║   ██║   ██║  ██║███████╗██║  ██║
   ╚══╝╚══╝ ╚══════╝╚═╝  ╚═╝   ╚═╝   ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝{C.RESET}
{C.GRAY}  ──────────────────────────────────────────────────────────{C.RESET}
{C.DIM}  Terminal Weather v1.0                        {C.GRAY}by mainstarkov{C.RESET}
{C.GRAY}  ──────────────────────────────────────────────────────────{C.RESET}
"""

W = 56
WEATHER_ICONS = {
    0: ("☀️",  "Clear sky"),
    1: ("🌤",  "Mainly clear"),
    2: ("⛅",  "Partly cloudy"),
    3: ("☁️",  "Overcast"),
    45: ("🌫", "Foggy"),
    48: ("🌫", "Icy fog"),
    51: ("🌦", "Light drizzle"),
    53: ("🌦", "Drizzle"),
    55: ("🌧", "Heavy drizzle"),
    61: ("🌧", "Light rain"),
    63: ("🌧", "Rain"),
    65: ("🌧", "Heavy rain"),
    71: ("🌨", "Light snow"),
    73: ("🌨", "Snow"),
    75: ("❄️",  "Heavy snow"),
    77: ("❄️",  "Snow grains"),
    80: ("🌧", "Rain showers"),
    81: ("🌧", "Moderate showers"),
    82: ("⛈",  "Violent showers"),
    85: ("🌨", "Snow showers"),
    86: ("🌨", "Heavy snow showers"),
    95: ("⛈",  "Thunderstorm"),
    96: ("⛈",  "Thunderstorm + hail"),
    99: ("⛈",  "Severe thunderstorm"),
}

def fetch_json(url: str) -> dict | None:
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "WeatherNow/1.0"})
        with urllib.request.urlopen(req, timeout=10) as r:
            return json.loads(r.read().decode())
    except Exception:
        return None


def geocode(city: str) -> dict | None:
    encoded = urllib.request.quote(city)
    data = fetch_json(
        f"https://geocoding-api.open-meteo.com/v1/search?name={encoded}&count=1&language=en"
    )
    if data and data.get("results"):
        r = data["results"][0]
        return {
            "name": r.get("name", city),
            "country": r.get("country", ""),
            "cc": r.get("country_code", ""),
            "lat": r["latitude"],
            "lon": r["longitude"],
            "tz": r.get("timezone", ""),
        }
    return None


def get_weather(lat: float, lon: float) -> dict | None:
    return fetch_json(
        f"https://api.open-meteo.com/v1/forecast?"
        f"latitude={lat}&longitude={lon}"
        f"&current=temperature_2m,relative_humidity_2m,apparent_temperature,"
        f"wind_speed_10m,wind_gusts_10m,wind_direction_10m,"
        f"weather_code,pressure_msl,cloud_cover,uv_index"
        f"&daily=weather_code,temperature_2m_max,temperature_2m_min,"
        f"precipitation_sum,wind_speed_10m_max"
        f"&timezone=auto&forecast_days=5"
    )

def box_top(title: str, icon: str, color: str):
    w = W - 2
    pad = max(0, w - 3 - len(title))
    print(f"\n{color}  {icon} ╔{'═' * w}╗{C.RESET}")
    print(f"{color}    ║  {C.BOLD}{title}{C.RESET}{color}{' ' * pad}║{C.RESET}")
    print(f"{color}    ╠{'═' * w}╣{C.RESET}")


def box_row(label: str, value: str, color: str, vc: str = ""):
    vc = vc or C.WHITE
    padded = f"{label:<16}"
    pad = max(0, W - 3 - 16 - 3 - len(value))
    print(f"{color}    ║  {C.YELLOW}{padded}{C.RESET} {vc}{value}{C.RESET}{' ' * pad}{color}║{C.RESET}")


def box_sep(color: str):
    print(f"{color}    ╟{'─' * (W - 2)}╢{C.RESET}")


def box_end(color: str):
    print(f"{color}    ╚{'═' * (W - 2)}╝{C.RESET}")


def flag(cc: str) -> str:
    if not cc or len(cc) != 2:
        return "🌐"
    return chr(0x1F1E6 + ord(cc[0]) - ord("A")) + chr(0x1F1E6 + ord(cc[1]) - ord("A"))


def wind_direction(deg: int) -> str:
    dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
            "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
    return dirs[round(deg / 22.5) % 16]


def temp_color(temp: float) -> str:
    if temp <= -10:
        return C.BLUE
    if temp <= 0:
        return C.CYAN
    if temp <= 15:
        return C.WHITE
    if temp <= 25:
        return C.YELLOW
    if temp <= 35:
        return C.RED
    return f"\033[91m"

def display_weather(location: dict, weather: dict):
    cur = weather.get("current", {})
    daily = weather.get("daily", {})

    temp = cur.get("temperature_2m", 0)
    feels = cur.get("apparent_temperature", 0)
    code = cur.get("weather_code", 0)
    icon, desc = WEATHER_ICONS.get(code, ("❓", "Unknown"))
    tc = temp_color(temp)

    # Current weather
    box_top(f"{flag(location['cc'])}  {location['name']}, {location['country']}", "📍", C.CYAN)
    box_row("Condition", f"{icon}  {desc}", C.CYAN)
    box_row("Temperature", f"{tc}{temp}°C{C.RESET}", C.CYAN)
    box_row("Feels like", f"{tc}{feels}°C{C.RESET}", C.CYAN)
    box_sep(C.CYAN)
    box_row("Humidity", f"{cur.get('relative_humidity_2m', 0)}%", C.CYAN)
    box_row("Pressure", f"{cur.get('pressure_msl', 0)} hPa", C.CYAN)
    box_row("Cloud cover", f"{cur.get('cloud_cover', 0)}%", C.CYAN)
    box_row("UV Index", f"{cur.get('uv_index', 0)}", C.CYAN)
    box_sep(C.CYAN)
    wind = cur.get("wind_speed_10m", 0)
    gusts = cur.get("wind_gusts_10m", 0)
    wdir = wind_direction(cur.get("wind_direction_10m", 0))
    box_row("Wind", f"{wind} km/h {wdir}", C.CYAN)
    box_row("Gusts", f"{gusts} km/h", C.CYAN)
    box_end(C.CYAN)

    # 5-day forecast
    dates = daily.get("time", [])
    maxs = daily.get("temperature_2m_max", [])
    mins = daily.get("temperature_2m_min", [])
    codes = daily.get("weather_code", [])
    precip = daily.get("precipitation_sum", [])
    winds = daily.get("wind_speed_10m_max", [])

    if dates:
        box_top("5-DAY FORECAST", "📅", C.GREEN)
        for i in range(min(5, len(dates))):
            d_icon, d_desc = WEATHER_ICONS.get(codes[i] if i < len(codes) else 0, ("❓", "?"))
            hi = maxs[i] if i < len(maxs) else "?"
            lo = mins[i] if i < len(mins) else "?"
            rain = precip[i] if i < len(precip) else 0
            w = winds[i] if i < len(winds) else 0

            hi_c = temp_color(hi) if isinstance(hi, (int, float)) else ""
            lo_c = temp_color(lo) if isinstance(lo, (int, float)) else ""

            forecast_str = f"{d_icon} {hi_c}{hi}°{C.RESET}/{lo_c}{lo}°{C.RESET}"
            if rain and rain > 0:
                forecast_str += f"  {C.BLUE}💧{rain}mm{C.RESET}"
            forecast_str += f"  💨{w}km/h"

            box_row(dates[i], forecast_str, C.GREEN)
        box_end(C.GREEN)

    print()

def main():
    parser = argparse.ArgumentParser(prog="weathernow", description="Terminal weather app")
    parser.add_argument("city", nargs="+", help="city name (e.g. Moscow, 'New York')")
    parser.add_argument("--json", action="store_true", help="output raw JSON")
    args = parser.parse_args()

    print(BANNER)
    city = " ".join(args.city)

    location = geocode(city)
    if not location:
        print(f"  {C.RED}✗ City not found: {city}{C.RESET}\n")
        sys.exit(1)

    weather = get_weather(location["lat"], location["lon"])
    if not weather:
        print(f"  {C.RED}✗ Could not fetch weather data{C.RESET}\n")
        sys.exit(1)

    if args.json:
        print(json.dumps({"location": location, "weather": weather}, indent=2, ensure_ascii=False))
    else:
        display_weather(location, weather)


if __name__ == "__main__":
    main()