mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Sync the workout log across devices using the shared crdt-sync / crdt_sync_dart CRDT transport (same scheme as diet_guard). - Python: new _workout_sync.py + _temperature_status_mixin.py; wire sync into screen_lock.py / status_view.py; add the crdt-sync git dependency and test_sync_fixtures / test_workout_sync suites. - Dart (workout_app): new workout_sync_service.dart, github_device_auth.dart, sync_settings.dart with tests + fakes; settings/workout screens updated. - docs: workstream-d-shared-crdt-transport.md. Verification: pytest 747 passed at 100% coverage; flutter analyze clean; flutter test passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
82 lines
3.2 KiB
Python
82 lines
3.2 KiB
Python
"""Temperature fetching via wttr.in for the heat-skip feature.
|
|
|
|
Pure logic — no Tk imports. Always fetches from the API; never trusts
|
|
user claims about the temperature.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from concurrent.futures import TimeoutError as _FutureTimeoutError
|
|
from dataclasses import dataclass
|
|
import json
|
|
import logging
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
_WTTR_URL = "https://wttr.in/{city}?format=j1"
|
|
_TIMEOUT_SECONDS = 5
|
|
# urlopen's `timeout` bounds the socket connect/read, but not necessarily DNS
|
|
# resolution (`getaddrinfo`) on every platform/resolver — a stalled resolver
|
|
# can block far past `_TIMEOUT_SECONDS` with no visible failure. This is a
|
|
# second, hard wall-clock ceiling enforced independently in a worker thread.
|
|
HARD_TIMEOUT_SECONDS = 6.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TemperatureCheck:
|
|
"""Result of a bounded live temperature fetch."""
|
|
|
|
temp_celsius: float | None
|
|
timed_out: bool
|
|
|
|
|
|
def _fetch_current_temp_celsius_unbounded(city: str) -> float | None:
|
|
"""Return the current temperature in °C for *city*, or None on failure.
|
|
|
|
Uses wttr.in's JSON API (no API key required). Returns None on network
|
|
errors, timeouts, or unexpected response shapes so callers can fail-closed.
|
|
May block past `_TIMEOUT_SECONDS` if DNS resolution stalls — callers
|
|
should go through `fetch_current_temp_with_status` for a hard bound
|
|
instead of calling this directly.
|
|
"""
|
|
url = _WTTR_URL.format(city=urllib.request.quote(city, safe=""))
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=_TIMEOUT_SECONDS) as resp:
|
|
data = json.loads(resp.read())
|
|
temp_str = data["current_condition"][0]["temp_C"]
|
|
return float(temp_str)
|
|
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
_logger.warning("Temperature fetch failed (network): %s", exc)
|
|
return None
|
|
except (KeyError, IndexError, ValueError, json.JSONDecodeError) as exc:
|
|
_logger.warning("Temperature fetch failed (parse): %s", exc)
|
|
return None
|
|
|
|
|
|
def fetch_current_temp_with_status(
|
|
city: str, *, hard_timeout: float = HARD_TIMEOUT_SECONDS
|
|
) -> TemperatureCheck:
|
|
"""Fetch °C for *city*, bounded to *hard_timeout* seconds wall-clock.
|
|
|
|
Runs the fetch in a worker thread and stops waiting on it after
|
|
*hard_timeout* regardless of what the network call is doing internally
|
|
(DNS, connect, read). The worker thread itself can't be killed and may
|
|
keep running in the background after we give up on it — acceptable for
|
|
a fire-and-forget diagnostic read. Distinguishes "timed out" from other
|
|
failures (network/parse errors) so callers can report which happened.
|
|
"""
|
|
executor = ThreadPoolExecutor(max_workers=1)
|
|
future = executor.submit(_fetch_current_temp_celsius_unbounded, city)
|
|
executor.shutdown(wait=False)
|
|
try:
|
|
temp = future.result(timeout=hard_timeout)
|
|
except _FutureTimeoutError:
|
|
_logger.warning(
|
|
"Temperature fetch exceeded hard timeout of %.1fs", hard_timeout
|
|
)
|
|
return TemperatureCheck(temp_celsius=None, timed_out=True)
|
|
return TemperatureCheck(temp_celsius=temp, timed_out=False)
|