feat(services): enhance services with rollback and observability
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -4,6 +4,9 @@ import os
|
||||
import platform
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
try:
|
||||
import psutil
|
||||
@@ -15,6 +18,10 @@ class SystemService:
|
||||
_last_net_bytes_sent: int | None = None
|
||||
_last_net_bytes_recv: int | None = None
|
||||
_last_net_sample_at: float | None = None
|
||||
_weather_cache: dict | None = None
|
||||
_weather_cached_at: float | None = None
|
||||
_weather_cached_location: str | None = None
|
||||
_weather_cache_ttl_seconds: float = 10 * 60 # 10 minutes
|
||||
|
||||
def __init__(self):
|
||||
# Import settings here to avoid circular imports
|
||||
@@ -134,8 +141,95 @@ class SystemService:
|
||||
'timestamp': datetime.now(UTC).isoformat(),
|
||||
}
|
||||
|
||||
def get_config(self) -> dict:
|
||||
async def _fetch_weather(self, location: str) -> dict:
|
||||
try:
|
||||
timeout = httpx.Timeout(10.0, connect=5.0)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.get(f'https://wttr.in/{location}', params={'format': 'j1'})
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
current = (payload.get('current_condition') or [{}])[0]
|
||||
weather_code = current.get('weatherCode')
|
||||
temp = current.get('temp_C')
|
||||
parsed_code = int(weather_code) if weather_code is not None and str(weather_code).isdigit() else None
|
||||
if parsed_code is None or temp in (None, ''):
|
||||
return {'weather_code': None, 'weather_summary': 'Weather unavailable'}
|
||||
|
||||
label = self._weather_code_label(parsed_code)
|
||||
return {
|
||||
'weather_code': parsed_code,
|
||||
'weather_summary': f'{label} {temp}°C',
|
||||
}
|
||||
except (httpx.HTTPError, ValueError, TypeError):
|
||||
return {'weather_code': None, 'weather_summary': 'Weather unavailable'}
|
||||
|
||||
@staticmethod
|
||||
def _weather_code_label(code: int | None) -> str:
|
||||
if code == 0:
|
||||
return 'Clear'
|
||||
if code in {1, 2}:
|
||||
return 'Partly Cloudy'
|
||||
if code == 3:
|
||||
return 'Overcast'
|
||||
if code in {45, 48}:
|
||||
return 'Fog'
|
||||
if code in {51, 53, 55, 56, 57}:
|
||||
return 'Drizzle'
|
||||
if code in {61, 63, 65, 66, 67, 80, 81, 82}:
|
||||
return 'Rain'
|
||||
if code in {71, 73, 75, 77, 85, 86}:
|
||||
return 'Snow'
|
||||
if code in {95, 96, 99}:
|
||||
return 'Thunderstorm'
|
||||
return 'Weather'
|
||||
|
||||
async def get_config(self) -> dict:
|
||||
"""Get public system configuration."""
|
||||
location = self._settings.LOCATION
|
||||
now = time.time()
|
||||
cached_weather = self.__class__._weather_cache
|
||||
cached_at = self.__class__._weather_cached_at
|
||||
cached_location = self.__class__._weather_cached_location
|
||||
|
||||
cache_is_valid = (
|
||||
cached_weather is not None
|
||||
and cached_at is not None
|
||||
and cached_location == location
|
||||
and (now - cached_at) < self.__class__._weather_cache_ttl_seconds
|
||||
)
|
||||
|
||||
if cache_is_valid:
|
||||
return {
|
||||
'location': location,
|
||||
**cached_weather,
|
||||
'weather_cached': True,
|
||||
'weather_cached_at': cached_at,
|
||||
}
|
||||
|
||||
weather = await self._fetch_weather(location)
|
||||
|
||||
# If fetch failed but we have *any* last known weather for same location, return it to avoid UI flicker.
|
||||
if (
|
||||
(weather.get('weather_code') is None)
|
||||
and cached_weather is not None
|
||||
and cached_location == location
|
||||
):
|
||||
return {
|
||||
'location': location,
|
||||
**cached_weather,
|
||||
'weather_cached': True,
|
||||
'weather_cached_at': cached_at,
|
||||
'weather_stale': True,
|
||||
}
|
||||
|
||||
# Update cache on successful/meaningful payload (or keep "unavailable" if never succeeded).
|
||||
self.__class__._weather_cache = weather
|
||||
self.__class__._weather_cached_at = now
|
||||
self.__class__._weather_cached_location = location
|
||||
|
||||
return {
|
||||
'location': self._settings.LOCATION,
|
||||
'location': location,
|
||||
**weather,
|
||||
'weather_cached': False,
|
||||
'weather_cached_at': now,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user