Create weather.py
Browse files- services/weather.py +104 -0
services/weather.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import Optional, Dict
|
| 3 |
+
import requests
|
| 4 |
+
from datetime import datetime, timezone
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
UA = {"User-Agent": "HF-Space-Trip-Planner/1.0 (+weather)"}
|
| 8 |
+
|
| 9 |
+
# WMO weather codes -> 簡易条件
|
| 10 |
+
# https://open-meteo.com/en/docs#api-formats
|
| 11 |
+
def _code_to_condition(code: int) -> str:
|
| 12 |
+
if code == 0:
|
| 13 |
+
return "sunny"
|
| 14 |
+
if 1 <= code <= 3:
|
| 15 |
+
return "cloudy"
|
| 16 |
+
if 45 <= code <= 48:
|
| 17 |
+
return "cloudy" # 霧・もや
|
| 18 |
+
if 51 <= code <= 67:
|
| 19 |
+
return "rainy" # 霧雨〜凍雨
|
| 20 |
+
if 71 <= code <= 77:
|
| 21 |
+
return "snowy" # 雪
|
| 22 |
+
if 80 <= code <= 82:
|
| 23 |
+
return "rainy" # にわか雨
|
| 24 |
+
if 85 <= code <= 86:
|
| 25 |
+
return "snowy" # にわか雪
|
| 26 |
+
if 95 <= code <= 99:
|
| 27 |
+
return "rainy" # 雷雨(豪雨側に丸め)
|
| 28 |
+
return "cloudy"
|
| 29 |
+
|
| 30 |
+
def _today_iso() -> str:
|
| 31 |
+
# Hugging Face の実行環境ではUTC基準になりがちだが、日付文字列だけ必要
|
| 32 |
+
return datetime.now(timezone.utc).date().isoformat()
|
| 33 |
+
|
| 34 |
+
def get_weather_summary(
|
| 35 |
+
lat: float,
|
| 36 |
+
lon: float,
|
| 37 |
+
date: Optional[str] = None,
|
| 38 |
+
override: Optional[str] = None,
|
| 39 |
+
) -> Dict[str, object]:
|
| 40 |
+
"""
|
| 41 |
+
外部API(Open-Meteo)を使って指定日の天気を簡約化して返す。
|
| 42 |
+
失敗時は 'cloudy' を返すフェイルセーフ。
|
| 43 |
+
返り値例:
|
| 44 |
+
{
|
| 45 |
+
"condition": "sunny|cloudy|rainy|snowy",
|
| 46 |
+
"temp_c": 27.3, # あれば
|
| 47 |
+
"source": "open-meteo",
|
| 48 |
+
"date": "YYYY-MM-DD"
|
| 49 |
+
}
|
| 50 |
+
"""
|
| 51 |
+
# UI のオーバーライド優先
|
| 52 |
+
if override and override != "(auto)":
|
| 53 |
+
return {"condition": override, "source": "override", "date": date or _today_iso()}
|
| 54 |
+
|
| 55 |
+
use_date = (date or _today_iso())
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
url = "https://api.open-meteo.com/v1/forecast"
|
| 59 |
+
params = {
|
| 60 |
+
"latitude": lat,
|
| 61 |
+
"longitude": lon,
|
| 62 |
+
"daily": "weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max",
|
| 63 |
+
"timezone": "auto",
|
| 64 |
+
"start_date": use_date,
|
| 65 |
+
"end_date": use_date,
|
| 66 |
+
}
|
| 67 |
+
r = requests.get(url, params=params, headers=UA, timeout=20)
|
| 68 |
+
r.raise_for_status()
|
| 69 |
+
j = r.json()
|
| 70 |
+
days = j.get("daily", {})
|
| 71 |
+
if not days:
|
| 72 |
+
raise RuntimeError("no daily in response")
|
| 73 |
+
|
| 74 |
+
codes = days.get("weathercode") or []
|
| 75 |
+
tmax = days.get("temperature_2m_max") or []
|
| 76 |
+
tmin = days.get("temperature_2m_min") or []
|
| 77 |
+
ppop = days.get("precipitation_probability_max") or []
|
| 78 |
+
|
| 79 |
+
code = int(codes[0]) if codes else 1
|
| 80 |
+
condition = _code_to_condition(code)
|
| 81 |
+
|
| 82 |
+
# 気温は平均っぽく
|
| 83 |
+
temp_c = None
|
| 84 |
+
if tmax and tmin:
|
| 85 |
+
temp_c = round((float(tmax[0]) + float(tmin[0])) / 2.0, 1)
|
| 86 |
+
elif tmax:
|
| 87 |
+
temp_c = round(float(tmax[0]), 1)
|
| 88 |
+
|
| 89 |
+
# 強い降水確率なら rainy に寄せる
|
| 90 |
+
if ppop:
|
| 91 |
+
try:
|
| 92 |
+
if int(ppop[0]) >= 60:
|
| 93 |
+
condition = "rainy" if condition != "snowy" else "snowy"
|
| 94 |
+
except Exception:
|
| 95 |
+
pass
|
| 96 |
+
|
| 97 |
+
out = {"condition": condition, "source": "open-meteo", "date": use_date}
|
| 98 |
+
if temp_c is not None:
|
| 99 |
+
out["temp_c"] = temp_c
|
| 100 |
+
return out
|
| 101 |
+
|
| 102 |
+
except Exception:
|
| 103 |
+
# API失敗時のフォールバック
|
| 104 |
+
return {"condition": "cloudy", "source": "fallback", "date": use_date}
|