feat: content provenance and complete frame management workflows
This commit is contained in:
@@ -17,3 +17,138 @@ class MockWeatherProvider:
|
||||
|
||||
def get_forecast(self):
|
||||
return []
|
||||
|
||||
|
||||
class ConfigurableWeatherProvider:
|
||||
"""On-demand public weather with a ten-minute cache and bounded error backoff."""
|
||||
|
||||
def __init__(self, db, transport=None):
|
||||
import threading
|
||||
|
||||
import requests
|
||||
|
||||
self.db = db
|
||||
self.transport = transport or requests.get
|
||||
self.lock = threading.Lock()
|
||||
self.cache = None
|
||||
self.next_fetch = 0.0
|
||||
self.cached_config = None
|
||||
|
||||
def config(self):
|
||||
import json
|
||||
|
||||
from pigway_frame_flow.models import WeatherConfig
|
||||
|
||||
row = self.db.one("SELECT payload FROM app_config WHERE key='weather'")
|
||||
return WeatherConfig.model_validate(json.loads(row["payload"]) if row else {})
|
||||
|
||||
def configure(self, config):
|
||||
from pigway_frame_flow.storage.database import encode
|
||||
|
||||
with self.lock:
|
||||
self.db.execute(
|
||||
"INSERT OR REPLACE INTO app_config VALUES('weather',?)",
|
||||
(encode(config.model_dump()),),
|
||||
)
|
||||
self.cache = None
|
||||
self.next_fetch = 0
|
||||
self.cached_config = None
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def summary(code):
|
||||
if code == 0:
|
||||
return "晴 / Clear"
|
||||
if code in (1, 2, 3):
|
||||
return "多云 / Cloudy"
|
||||
if code in (45, 48):
|
||||
return "雾 / Fog"
|
||||
if code in (71, 73, 75, 77, 85, 86):
|
||||
return "雪 / Snow"
|
||||
if code in (95, 96, 99):
|
||||
return "雷雨 / Thunderstorm"
|
||||
if code in (51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82):
|
||||
return "雨 / Rain"
|
||||
return "未知 / Unknown"
|
||||
|
||||
def fetch(self):
|
||||
import copy
|
||||
import time
|
||||
|
||||
config = self.config()
|
||||
if config.provider == "mock":
|
||||
current = MockWeatherProvider().get_current()
|
||||
current["location"] = config.location
|
||||
return {"current": current, "forecast": []}
|
||||
with self.lock:
|
||||
if self.cached_config != config:
|
||||
self.cache = None
|
||||
self.next_fetch = 0
|
||||
self.cached_config = config
|
||||
if time.monotonic() < self.next_fetch:
|
||||
return copy.deepcopy(self.cache)
|
||||
try:
|
||||
response = self.transport(
|
||||
"https://api.open-meteo.com/v1/forecast",
|
||||
params={
|
||||
"latitude": config.latitude,
|
||||
"longitude": config.longitude,
|
||||
"current": "temperature_2m,weather_code,relative_humidity_2m,wind_speed_10m",
|
||||
"daily": "weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"forecast_days": 3,
|
||||
},
|
||||
timeout=8,
|
||||
allow_redirects=False,
|
||||
)
|
||||
response.raise_for_status()
|
||||
raw = response.json()
|
||||
cur, daily = raw["current"], raw["daily"]
|
||||
current = {
|
||||
"location": config.location,
|
||||
"temperature": f"{cur['temperature_2m']}°C",
|
||||
"summary": self.summary(cur["weather_code"]),
|
||||
"humidity": cur.get("relative_humidity_2m"),
|
||||
"wind_kmh": cur.get("wind_speed_10m"),
|
||||
"observed_at": cur["time"] + "+08:00",
|
||||
"mock": False,
|
||||
"stale": False,
|
||||
"available": True,
|
||||
"attribution": "Open-Meteo / CC BY 4.0",
|
||||
}
|
||||
forecast = [
|
||||
{
|
||||
"date": day,
|
||||
"summary": self.summary(daily["weather_code"][i]),
|
||||
"minimum_c": daily["temperature_2m_min"][i],
|
||||
"maximum_c": daily["temperature_2m_max"][i],
|
||||
"rain_probability": daily["precipitation_probability_max"][i],
|
||||
}
|
||||
for i, day in enumerate(daily["time"])
|
||||
]
|
||||
self.cache = {"current": current, "forecast": forecast}
|
||||
self.next_fetch = time.monotonic() + 600
|
||||
except Exception:
|
||||
if self.cache:
|
||||
self.cache["current"]["stale"] = True
|
||||
else:
|
||||
self.cache = {
|
||||
"current": {
|
||||
"location": config.location,
|
||||
"temperature": "--",
|
||||
"summary": "天气暂不可用 / Weather unavailable",
|
||||
"mock": False,
|
||||
"stale": True,
|
||||
"available": False,
|
||||
"attribution": "Open-Meteo / CC BY 4.0",
|
||||
},
|
||||
"forecast": [],
|
||||
}
|
||||
self.next_fetch = time.monotonic() + 120
|
||||
return copy.deepcopy(self.cache)
|
||||
|
||||
def get_current(self):
|
||||
return self.fetch()["current"]
|
||||
|
||||
def get_forecast(self):
|
||||
return self.fetch()["forecast"]
|
||||
|
||||
Reference in New Issue
Block a user