Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions tests/test_weather_context.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import base64
import logging
from typing import TypeGuard

import httpx
Expand All @@ -21,6 +22,7 @@
OpenMeteoProvider,
QWeatherJWTAuthenticator,
QWeatherProvider,
UnsupportedForecastDateError,
WeatherContextError,
_format_qweather_day,
_format_qweather_lifestyle,
Expand Down Expand Up @@ -834,6 +836,56 @@ async def fetch(self, latitude: float, longitude: float) -> WeatherContextSnapsh
assert "sensitive upstream detail" not in caplog.text


async def test_logged_provider_logs_unsupported_forecast_date_as_skip(caplog) -> None:
class UndatedProvider:
async def fetch(self, latitude: float, longitude: float) -> WeatherContextSnapshot:
raise AssertionError("Dated requests must not call the current-weather method") # pragma: no cover

provider = LoggedWeatherContextProvider("nea-sg", UndatedProvider())

with (
caplog.at_level("INFO", logger="weather_briefing.weather_context"),
pytest.raises(UnsupportedForecastDateError, match="does not support target forecast dates"),
):
await provider.fetch(1, 2, forecast_date=pendulum.date(2026, 7, 21))

assert "Weather API call skipped provider=nea-sg" in caplog.text
assert "forecast_date=2026-07-21" in caplog.text
assert "reason=unsupported-forecast-date" in caplog.text
assert "Weather API call failed provider=nea-sg" not in caplog.text


@pytest.mark.parametrize("secondary_failure", ("elapsed", "logger"))
async def test_logged_provider_preserves_unsupported_date_when_skip_logging_fails(
monkeypatch,
secondary_failure: str,
) -> None:
class UndatedProvider:
async def fetch(self, latitude: float, longitude: float) -> WeatherContextSnapshot:
raise AssertionError("Dated requests must not call the current-weather method") # pragma: no cover

if secondary_failure == "elapsed":

def fail_elapsed(started_at: float) -> int:
raise RuntimeError("secondary failure")

monkeypatch.setattr("weather_briefing.weather_context._elapsed_milliseconds", fail_elapsed)
else:
original_info = logging.getLogger("weather_briefing.weather_context").info

def fail_skip_log(message: str, *args: object) -> None:
if "skipped" in message:
raise RuntimeError("secondary failure")
original_info(message, *args)

monkeypatch.setattr("weather_briefing.weather_context._LOGGER.info", fail_skip_log)

provider = LoggedWeatherContextProvider("nea-sg", UndatedProvider())

with pytest.raises(UnsupportedForecastDateError, match="does not support target forecast dates"):
await provider.fetch(1, 2, forecast_date=pendulum.date(2026, 7, 21))


async def test_missing_weather_air_quality_requires_optional_aqicn_configuration() -> None:
snapshot = WeatherContextSnapshot(
source_id="weather:test",
Expand Down
18 changes: 16 additions & 2 deletions weather_briefing/weather_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ class WeatherContextError(RuntimeError):
"""Raised when a weather source is unavailable or violates its contract."""


class UnsupportedForecastDateError(WeatherContextError):
"""Raised when a provider cannot fetch an explicit forecast date."""


class _QWeatherResponseError(ValueError):
"""Raised for safe, code-defined QWeather response contract errors."""

Expand Down Expand Up @@ -179,6 +183,16 @@ async def fetch(
_LOGGER.info("Weather API call started provider=%s", self._name)
try:
snapshot = await fetch_weather_context(self._provider, latitude, longitude, forecast_date)
except UnsupportedForecastDateError:
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
with suppress(Exception):
_LOGGER.info(
"Weather API call skipped provider=%s duration_ms=%d forecast_date=%s "
"reason=unsupported-forecast-date",
self._name,
_elapsed_milliseconds(started_at),
forecast_date,
)
raise
except WeatherContextError as exc:
_LOGGER.warning(
"Weather API call failed provider=%s duration_ms=%d reason=%s",
Expand Down Expand Up @@ -785,10 +799,10 @@ async def fetch_weather_context(
if forecast_date is None:
return await provider.fetch(latitude, longitude)
if not isinstance(provider, DatedWeatherContextProvider):
raise WeatherContextError(f"{type(provider).__name__} does not support target forecast dates")
raise UnsupportedForecastDateError(f"{type(provider).__name__} does not support target forecast dates")
fetch_for_date = provider.fetch_for_date
if not callable(fetch_for_date):
raise WeatherContextError(f"{type(provider).__name__} does not support target forecast dates")
raise UnsupportedForecastDateError(f"{type(provider).__name__} does not support target forecast dates")
return await fetch_for_date(latitude, longitude, forecast_date)


Expand Down