refactor(rain): enhance fetch_precip_series_nws with caching support

- Added cache_ttl parameter to fetch_precip_series_nws to enable caching of results for improved performance.
- Implemented logic to reuse cached results based on location and cache expiration.
- Updated tests to ensure proper handling of cache_ttl without causing errors.
- Refactored WorldCupFastcastClient to streamline connection handling and improve readability.
This commit is contained in:
agessaman
2026-06-15 15:26:55 -07:00
parent b45b23bdd2
commit 4bf60622ff
4 changed files with 94 additions and 9 deletions
+4 -4
View File
@@ -105,9 +105,9 @@ class WorldCupFastcastClient:
async def _connect_once(self) -> None:
headers = {"Origin": ORIGIN, "User-Agent": USER_AGENT}
async with aiohttp.ClientSession(headers=headers) as session:
timeout = aiohttp.ClientTimeout(total=self.connect_timeout)
async with session.get(WEBSOCKET_HOST_URL, timeout=timeout) as resp:
timeout = aiohttp.ClientTimeout(total=self.connect_timeout)
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
async with session.get(WEBSOCKET_HOST_URL) as resp:
resp.raise_for_status()
host = await resp.json()
@@ -117,7 +117,7 @@ class WorldCupFastcastClient:
raise ValueError("fastcast websockethost returned no ip")
url = f"wss://{ip}:{port}/FastcastService/pubsub/profiles/{self.profile}?TrafficId={uuid.uuid4()}"
async with session.ws_connect(url, timeout=self.connect_timeout, heartbeat=25, origin=ORIGIN) as ws:
async with session.ws_connect(url, heartbeat=25, origin=ORIGIN) as ws:
self.logger.info("Fastcast connected (topic=%s)", self.topic)
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
+16 -1
View File
@@ -486,6 +486,7 @@ def fetch_precip_series_nws(
timeout: int = 10,
logger: Any = None,
pop_floor: int = 50,
cache_ttl: float = 0.0,
) -> Optional[dict]:
"""Build a precip nowcast series from the NWS gridpoint forecast (US only).
@@ -499,7 +500,16 @@ def fetch_precip_series_nws(
probability rather than snapping to coarse 6-hour QPF boundaries, and a trace
of QPF at a low chance is not reported as rain. Times are naive UTC ISO strings
(they only need to be self-consistent: the nowcast works on relative minutes).
When cache_ttl > 0, a fresh prior result for the same rounded location is reused
(shared bounded cache with fetch_precip_series).
"""
cache_key = (round(lat, 2), round(lon, 2), "nws")
if cache_ttl > 0:
hit = _SERIES_CACHE.get(cache_key)
if hit is not None and (time.time() - hit[0]) < cache_ttl:
return hit[1]
headers = {"User-Agent": "(meshcore-bot, weather-nowcast)", "Accept": "application/geo+json"}
try:
pts = session.get(
@@ -547,7 +557,7 @@ def fetch_precip_series_nws(
precip.append(amt)
codes.append(_nws_weather_code(wx.get(h)) if amt else None)
return {
result = {
"times": times,
"precip": precip,
"codes": codes,
@@ -556,6 +566,11 @@ def fetch_precip_series_nws(
"current_code": codes[0] if codes else None,
"step": 60,
}
if cache_ttl > 0:
if len(_SERIES_CACHE) >= _SERIES_CACHE_CAP:
_SERIES_CACHE.pop(next(iter(_SERIES_CACHE)))
_SERIES_CACHE[cache_key] = (time.time(), result)
return result
@dataclass
+2 -4
View File
@@ -15,7 +15,7 @@ World Cup data is also reachable year-round through the `sports` command
front end with live scores, standings, and nation lookups.
"""
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
from ..clients.espn_client import ESPNClient
from ..clients.sports_mappings import SPORT_EMOJIS
@@ -50,8 +50,6 @@ class WorldCupCommand(BaseCommand):
{"name": "nation", "description": "A nation's matches (e.g. brazil, usa)"},
]
espn_client: Optional[ESPNClient] = None
def __init__(self, bot: "MeshCoreBot"):
"""Initialize the World Cup command with an ESPN client and data helper."""
super().__init__(bot)
@@ -62,7 +60,7 @@ class WorldCupCommand(BaseCommand):
cache_ttl_minutes = self.get_config_value("Worldcup_Command", "cache_ttl_minutes", fallback=360, value_type="int")
self.espn_client = ESPNClient(logger=self.logger, timeout=self.url_timeout)
self.espn_client: ESPNClient = ESPNClient(logger=self.logger, timeout=self.url_timeout)
self.wc_data = WorldCupData(self.espn_client, logger=self.logger, cache_ttl=cache_ttl_minutes * 60)
def matches_keyword(self, message: MeshMessage) -> bool: