init
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import datetime
|
||||
from math import inf
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from cnet import HttpError, as_dict, as_float, as_int, as_list, as_str, fetch_json
|
||||
from db_models import DailyForecast
|
||||
from geo_locator import Geo
|
||||
|
||||
|
||||
messages = {
|
||||
401: "[401] Wrong WEATHER_API_KEY",
|
||||
404: "[404] City not found",
|
||||
429: "[429] Limit exceeded",
|
||||
}
|
||||
|
||||
|
||||
class WeatherAPIError(Exception): ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Item:
|
||||
date: datetime.date
|
||||
hour: int
|
||||
temp_min: float
|
||||
temp_max: float
|
||||
humidity: float
|
||||
wind_speed: float
|
||||
description: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class _DayMetrics:
|
||||
t_mins: float = inf
|
||||
t_maxs: float = -inf
|
||||
h_sum: float = 0
|
||||
n: int = 0
|
||||
wind_max: float = -inf
|
||||
noon_dst: int | None = None
|
||||
description: str = ""
|
||||
|
||||
|
||||
def _timezone_offset(city: dict[str, object]) -> datetime.timedelta:
|
||||
shift = as_float(city.get("timezone"), default=0.0) or 0.0
|
||||
return datetime.timedelta(seconds=int(shift))
|
||||
|
||||
|
||||
def _parse_item(raw: object, offset: datetime.timedelta) -> _Item | None:
|
||||
entry = as_dict(raw)
|
||||
main = as_dict(entry.get("main"))
|
||||
wind = as_dict(entry.get("wind"))
|
||||
weather_list = as_list(entry.get("weather"))
|
||||
weather = as_dict(weather_list[0]) if weather_list else {}
|
||||
|
||||
dt = as_float(entry.get("dt"), default=0.0)
|
||||
if dt is None or dt <= 0:
|
||||
return None
|
||||
time = datetime.datetime.fromtimestamp(dt, tz=datetime.timezone.utc) + offset
|
||||
|
||||
return _Item(
|
||||
date=time.date(),
|
||||
hour=time.hour,
|
||||
temp_min=as_float(main.get("temp_min"), default=0.0) or 0.0,
|
||||
temp_max=as_float(main.get("temp_max"), default=0.0) or 0.0,
|
||||
humidity=as_float(main.get("humidity"), default=0.0) or 0.0,
|
||||
wind_speed=as_float(wind.get("speed"), default=0.0) or 0.0,
|
||||
description=as_str(weather.get("description"), default=""),
|
||||
)
|
||||
|
||||
|
||||
def _calc(intervals: list[_Item], city: str) -> list[DailyForecast]:
|
||||
days_metrics: dict[datetime.date, _DayMetrics] = {}
|
||||
for i in intervals:
|
||||
m = days_metrics.setdefault(i.date, _DayMetrics())
|
||||
m.t_mins = min(m.t_mins, i.temp_min)
|
||||
m.t_maxs = max(m.t_maxs, i.temp_max)
|
||||
|
||||
m.h_sum += i.humidity
|
||||
m.n += 1
|
||||
m.wind_max = max(m.wind_max, i.wind_speed)
|
||||
dst = abs(i.hour - 12)
|
||||
if m.noon_dst is None or dst < m.noon_dst:
|
||||
m.noon_dst = dst
|
||||
m.description = i.description
|
||||
|
||||
result: list[DailyForecast] = []
|
||||
for day, m in sorted(days_metrics.items()):
|
||||
result.append(
|
||||
DailyForecast(
|
||||
city=city,
|
||||
forecast_date=day,
|
||||
temp_min=m.t_mins,
|
||||
temp_max=m.t_maxs,
|
||||
humidity=round(m.h_sum / m.n) if m.n else 0,
|
||||
wind_speed=m.wind_max,
|
||||
description=m.description,
|
||||
)
|
||||
)
|
||||
return result[:4]
|
||||
|
||||
|
||||
def get_forecast(geo: Geo) -> list[DailyForecast]:
|
||||
api_key = os.getenv("WEATHER_API_KEY", "").strip()
|
||||
if not api_key:
|
||||
raise WeatherAPIError("no WEATHER_API_KEY")
|
||||
base_url = os.getenv(
|
||||
"WEATHER_API_BASE_URL", "https://api.openweathermap.org/data/2.5/forecast"
|
||||
)
|
||||
|
||||
params: dict[str, str] = {"appid": api_key, "units": "metric", "lang": "ru"}
|
||||
if geo.lat is not None and geo.lon is not None:
|
||||
params["lat"] = f"{geo.lat}"
|
||||
params["lon"] = f"{geo.lon}"
|
||||
else:
|
||||
params["q"] = geo.city
|
||||
|
||||
url = f"{base_url}?{urlencode(params)}"
|
||||
|
||||
try:
|
||||
raw = fetch_json(url)
|
||||
except HttpError as e:
|
||||
if e.status_code == HttpError.NO_CODE:
|
||||
raise WeatherAPIError(f"weather request failed: {e.body}") from e
|
||||
raise WeatherAPIError(
|
||||
messages.get(e.status_code, f"[{e.status_code}] weather error")
|
||||
) from e
|
||||
|
||||
resp = as_dict(raw)
|
||||
|
||||
cod = as_int(resp.get("cod"), 200)
|
||||
if cod != 200:
|
||||
raise WeatherAPIError(messages.get(cod, f"[{cod}] weather error"))
|
||||
|
||||
resp_city = as_dict(resp.get("city"))
|
||||
city = as_str(resp_city.get("name"), default=geo.city)
|
||||
offset = _timezone_offset(resp_city)
|
||||
items = [
|
||||
i
|
||||
for i in (_parse_item(entry, offset) for entry in as_list(resp.get("list")))
|
||||
if i is not None
|
||||
]
|
||||
if not items:
|
||||
raise WeatherAPIError("weather is empty")
|
||||
|
||||
return _calc(items, city)
|
||||
Reference in New Issue
Block a user