screen-locker/screen_locker/_temperature.py
Krzysztof kuhy Rudnicki 399f9815c4
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Add phone-workout GitHub sync via shared crdt_sync (Workstream C)
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
2026-07-11 00:06:59 +02:00

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)