11"""Handles loading and validating weather.gov astronomical data from file and API."""
22
3- import httpx
3+ from __future__ import annotations
4+
45from pydantic import ValidationError
56
6- from sample_python_app .core import setup_logger
7+ from sample_python_app .core import setup_logger , weather_settings
78from sample_python_app .models import (
89 AstronomicalData ,
910 ForecastFeature ,
1011 WeatherGovFeature ,
1112)
13+ from sample_python_app .services .http_client import CustomHTTPClient
14+
15+ weather_client = CustomHTTPClient (
16+ headers = weather_settings .WEATHER_HEADERS , base_url = weather_settings .WEATHER_API_BASE
17+ )
18+
1219
20+ def resolve_point_metadata (
21+ lat : float , lon : float , client : CustomHTTPClient
22+ ) -> WeatherGovFeature :
23+ """Resolve and return the WeatherGovFeature for the given lat/lon.
1324
14- def fetch_astronomical_data_from_api (lat : float , lon : float ) -> AstronomicalData :
25+ This calls the `/points/{lat},{lon}` endpoint and returns the
26+ validated `WeatherGovFeature` model. Other functions can call this
27+ to discover grid coordinates or forecast URLs.
28+ """
29+ logger = setup_logger (mode = "silent" )
30+ api_client = client or weather_client
31+ points_path = f"/points/{ lat } ,{ lon } "
32+ logger .info (
33+ "Resolving point metadata for coordinates %s,%s: %s" , lat , lon , points_path
34+ )
35+ points_data = api_client .get_json (points_path )
36+ point_model = WeatherGovFeature .model_validate (points_data )
37+ logger .info (
38+ "Resolved point metadata: grid=%s x=%s y=%s" ,
39+ point_model .properties .grid_id ,
40+ point_model .properties .grid_x ,
41+ point_model .properties .grid_y ,
42+ )
43+ return point_model
44+
45+
46+ def fetch_astronomical_data_from_api (
47+ lat : float , lon : float , client : CustomHTTPClient
48+ ) -> AstronomicalData :
1549 """Fetch and validate astronomical data from weather.gov API for given coordinates.
1650
1751 Args:
1852 lat (float): Latitude of the location.
1953 lon (float): Longitude of the location.
54+ client (HTTPClient, optional): HTTP client to use for requests. If
55+ not provided the module-level `weather_client` will be used.
2056
2157 Returns:
2258 AstronomicalData: Validated astronomical data from API response.
@@ -27,24 +63,23 @@ def fetch_astronomical_data_from_api(lat: float, lon: float) -> AstronomicalData
2763
2864 """
2965 logger = setup_logger (mode = "silent" )
30- url = f"https://api.weather.gov/points/{ lat } ,{ lon } "
31- headers = {"User-Agent" : "(milsman2, milsman2@gmail.com)" }
32- logger .info (f"Fetching astronomical data from URL: { url } " )
33- logger .info (f"Request headers: { headers } " )
66+ points_path = f"/points/{ lat } ,{ lon } "
67+ client = client or weather_client
68+ logger .info ("Fetching astronomical data from: %s" , points_path )
3469 try :
35- response = httpx .get (url , headers = headers )
36- response .raise_for_status ()
37- data = response .json ()
70+ data = client .get_json (points_path )
3871 model = WeatherGovFeature .model_validate (data )
3972 astro = model .properties .astronomical_data
4073 logger .info ("AstronomicalData fetched and validated from API." )
4174 return astro
4275 except ValidationError as e :
43- logger .error (f "Data validation error: { e } " )
76+ logger .error ("Data validation error: %s" , e )
4477 raise
4578
4679
47- def fetch_hourly_forecast_from_api (lat : float , lon : float ) -> ForecastFeature :
80+ def fetch_hourly_forecast_from_api (
81+ lat : float , lon : float , client : CustomHTTPClient
82+ ) -> ForecastFeature :
4883 """Fetch hourly forecast Feature for the given coordinates.
4984
5085 This function first queries the `/points/{lat},{lon}` endpoint to resolve
@@ -54,6 +89,8 @@ def fetch_hourly_forecast_from_api(lat: float, lon: float) -> ForecastFeature:
5489 Args:
5590 lat (float): Latitude of the location.
5691 lon (float): Longitude of the location.
92+ client (HTTPClient, optional): HTTP client to use for requests. If
93+ not provided the module-level `weather_client` will be used.
5794
5895 Returns:
5996 ForecastFeature: Parsed and validated hourly forecast feature.
@@ -64,30 +101,24 @@ def fetch_hourly_forecast_from_api(lat: float, lon: float) -> ForecastFeature:
64101
65102 """
66103 logger = setup_logger (mode = "silent" )
67- headers = { "User-Agent" : "(milsman2, milsman2@gmail.com)" }
104+ client = client or weather_client
68105
69106 # Resolve point metadata to find the hourly forecast URL
70- points_url = f"https://api.weather.gov/points/{ lat } ,{ lon } "
71- logger .info (f"Resolving grid for coordinates { lat } ,{ lon } : { points_url } " )
72- resp = httpx .get (points_url , headers = headers )
73- resp .raise_for_status ()
74- points_data = resp .json ()
75-
76- point_model = WeatherGovFeature .model_validate (points_data )
107+ point_model = resolve_point_metadata (lat , lon , client = client )
77108 forecast_url = point_model .properties .forecast_hourly
78- logger .info (f "Fetching hourly forecast from: { forecast_url } " )
109+ logger .info ("Fetching hourly forecast from: %s" , forecast_url )
79110
80- resp2 = httpx . get ( forecast_url , headers = headers )
81- resp2 . raise_for_status ()
82- forecast_data = resp2 . json ( )
111+ # forecast_url is often an absolute URL returned by the API; httpx
112+ # client with a base_url accepts absolute URLs as well, so pass through.
113+ forecast_data = client . get_json ( forecast_url )
83114
84115 forecast_model = ForecastFeature .model_validate (forecast_data )
85116 logger .info ("Hourly forecast fetched and validated." )
86117 return forecast_model
87118
88119
89120def fetch_hourly_forecast_by_grid (
90- grid_id : str , grid_x : int , grid_y : int
121+ grid_id : str , grid_x : int , grid_y : int , client : CustomHTTPClient
91122) -> ForecastFeature :
92123 """Fetch hourly forecast Feature directly from grid coordinates.
93124
@@ -99,22 +130,19 @@ def fetch_hourly_forecast_by_grid(
99130 grid_id: Grid identifier (e.g. "HGX").
100131 grid_x: Grid X coordinate (e.g. 59).
101132 grid_y: Grid Y coordinate (e.g. 98).
133+ client (HTTPClient, optional): HTTP client to use for requests. If
134+ not provided the module-level `weather_client` will be used.
102135
103136 Returns:
104137 ForecastFeature: Parsed and validated hourly forecast feature.
105138
106139 """
107140 logger = setup_logger (mode = "silent" )
108- headers = { "User-Agent" : "(milsman2, milsman2@gmail.com)" }
141+ client = client or weather_client
109142
110- url = (
111- "https://api.weather.gov/gridpoints/ "
112- f"{ grid_id } /{ grid_x } ,{ grid_y } /forecast/hourly"
113- )
114- logger .info (f"Fetching hourly forecast by grid from: { url } " )
115- resp = httpx .get (url , headers = headers )
116- resp .raise_for_status ()
117- data = resp .json ()
143+ path = f"/gridpoints/{ grid_id } /{ grid_x } ,{ grid_y } /forecast/hourly"
144+ logger .info ("Fetching hourly forecast by grid from: %s" , path )
145+ data = client .get_json (path )
118146
119147 model = ForecastFeature .model_validate (data )
120148 logger .info ("Hourly forecast (grid) fetched and validated." )
0 commit comments