-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrunner.py
More file actions
118 lines (99 loc) · 3.93 KB
/
Copy pathrunner.py
File metadata and controls
118 lines (99 loc) · 3.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"""Runner module for the sample Python app.
Handles fetching, validation, and display of astronomical data.
"""
import json
import time
from datetime import date
import httpx
from pydantic import ValidationError
from sample_python_app.core import (
FETCH_COUNTER,
FETCH_DURATION,
FETCH_ERRORS,
setup_logger,
weather_settings,
)
from sample_python_app.exceptions import AppError
from sample_python_app.services import (
fetch_astronomical_data_from_api,
fetch_hourly_forecast_from_api,
)
from sample_python_app.services.http_client import CustomHTTPClient
from sample_python_app.ui import display_astronomical_data
logger = setup_logger("normal")
class AstroFetcher:
"""Fetches astronomical data and displays only once per day.
Accepts an `HTTPClient` to use for all outbound requests so the
runner can own the client's lifecycle and tests can inject mocks.
"""
def __init__(self, client: CustomHTTPClient) -> None:
"""Initialize the AstroFetcher with an optional HTTP client."""
self._last_displayed_day: str | None = None
self.client = client
def fetch(self, *, exit_on_error: bool = True) -> None:
"""Fetch astronomical data and display if not already displayed today."""
lat = weather_settings.LOCATION.latitude
lon = weather_settings.LOCATION.longitude
logger.info(f"Using latitude={lat} longitude={lon}")
start = time.time()
try:
astro = fetch_astronomical_data_from_api(lat, lon, client=self.client)
forecast = fetch_hourly_forecast_from_api(lat, lon, client=self.client)
FETCH_COUNTER.inc()
except (
httpx.HTTPStatusError,
httpx.RequestError,
ValidationError,
json.JSONDecodeError,
AppError,
) as exc:
self._handle_fetch_error(exc, exit_on_error)
return
finally:
FETCH_DURATION.observe(time.time() - start)
today_str = date.today().isoformat()
if self._last_displayed_day != today_str:
display_astronomical_data(astro, forecast)
self._last_displayed_day = today_str
def reset_display(self):
"""Reset the last displayed day so display will occur again."""
self._last_displayed_day = None
def close(self) -> None:
"""Close the associated HTTP client if present.
This allows the runner to delegate shutdown responsibility to the
fetcher when it owns the client's lifecycle.
"""
if hasattr(self, "client") and self.client is not None:
try:
self.client.close()
except Exception:
logger.exception("Error closing HTTP client in AstroFetcher")
def _handle_fetch_error(self, exc: Exception, exit_on_error: bool) -> None:
FETCH_ERRORS.inc()
if isinstance(exc, httpx.HTTPStatusError):
logger.error("HTTP status error: %s", exc)
elif isinstance(exc, httpx.RequestError):
logger.error("Network error: %s", exc)
elif isinstance(exc, ValidationError):
logger.error("Validation error: %s", exc)
elif isinstance(exc, json.JSONDecodeError):
logger.error("JSON decode error: %s", exc)
else:
logger.exception("Unexpected error")
if exit_on_error:
raise SystemExit(1) from exc
raise AppError(str(exc)) from exc
runner_client = CustomHTTPClient(
headers=weather_settings.WEATHER_HEADERS,
base_url=weather_settings.WEATHER_API_BASE,
)
fetcher = AstroFetcher(client=runner_client)
def shutdown_runner() -> None:
"""Shutdown helper to close long-lived resources owned by the runner.
Call this from application shutdown hooks to ensure the HTTP client is
properly closed and connections are released.
"""
try:
fetcher.close()
except Exception:
logger.exception("Error during runner shutdown")