diff --git a/src/accessiweather/weather_client_base.py b/src/accessiweather/weather_client_base.py index 7f7e64a4..60a8c4a4 100644 --- a/src/accessiweather/weather_client_base.py +++ b/src/accessiweather/weather_client_base.py @@ -31,11 +31,9 @@ CurrentConditions, Forecast, HourlyForecast, - HourlyForecastPeriod, Location, SourceAttribution, SourceData, - TrendInsight, WeatherAlerts, WeatherData, ) @@ -912,16 +910,18 @@ def _launch_enrichment_tasks( # Smart enrichments for auto mode if self.data_source == "auto": tasks["sunrise_sunset"] = asyncio.create_task( - self._enrich_with_sunrise_sunset(weather_data, location) + enrichment.enrich_with_sunrise_sunset(self, weather_data, location) ) tasks["nws_discussion"] = asyncio.create_task( - self._enrich_with_nws_discussion(weather_data, location) + enrichment.enrich_with_nws_discussion(self, weather_data, location) ) tasks["vc_alerts"] = asyncio.create_task( - self._enrich_with_visual_crossing_alerts(weather_data, location, skip_notifications) + enrichment.enrich_with_visual_crossing_alerts( + self, weather_data, location, skip_notifications + ) ) tasks["vc_moon_data"] = asyncio.create_task( - self._enrich_with_visual_crossing_moon_data(weather_data, location) + enrichment.enrich_with_visual_crossing_moon_data(self, weather_data, location) ) if self.trend_insights_enabled and not weather_data.daily_history: @@ -931,10 +931,10 @@ def _launch_enrichment_tasks( # Post-processing enrichments (always run) tasks["environmental"] = asyncio.create_task( - self._populate_environmental_metrics(weather_data, location) + enrichment.populate_environmental_metrics(self, weather_data, location) ) tasks["aviation"] = asyncio.create_task( - self._enrich_with_aviation_data(weather_data, location) + enrichment.enrich_with_aviation_data(self, weather_data, location) ) return tasks @@ -960,7 +960,12 @@ async def _await_enrichments( logger.debug(f"Enrichment '{task_name}' failed: {result}") # Apply final processing - self._apply_trend_insights(weather_data) + trends.apply_trend_insights( # pragma: no cover + weather_data, + self.trend_insights_enabled, + self.trend_hours, + include_pressure=self.show_pressure_trend, + ) self._persist_weather_data(weather_data.location, weather_data) def _determine_api_choice(self, location: Location) -> str: @@ -1128,36 +1133,6 @@ async def _get_openmeteo_hourly_forecast(self, location: Location) -> HourlyFore location, self.openmeteo_base_url, self.timeout, self._get_http_client() ) - def _parse_nws_current_conditions(self, data: dict) -> CurrentConditions: - """Delegate to the NWS client module.""" - return nws_client.parse_nws_current_conditions(data) - - def _parse_nws_forecast(self, data: dict) -> Forecast: - """Delegate to the NWS client module.""" - return nws_client.parse_nws_forecast(data) - - def _parse_nws_alerts(self, data: dict) -> WeatherAlerts: - """Delegate to the NWS client module.""" - return nws_client.parse_nws_alerts(data) - - def _parse_nws_hourly_forecast( - self, data: dict, location: Location | None = None - ) -> HourlyForecast: - """Delegate to the NWS client module.""" - return nws_client.parse_nws_hourly_forecast(data, location) - - def _parse_openmeteo_current_conditions(self, data: dict) -> CurrentConditions: - """Delegate to the Open-Meteo client module.""" - return openmeteo_client.parse_openmeteo_current_conditions(data) - - def _parse_openmeteo_forecast(self, data: dict) -> Forecast: - """Delegate to the Open-Meteo client module.""" - return openmeteo_client.parse_openmeteo_forecast(data) - - def _parse_openmeteo_hourly_forecast(self, data: dict) -> HourlyForecast: - """Delegate to the Open-Meteo client module.""" - return openmeteo_client.parse_openmeteo_hourly_forecast(data) - async def _augment_current_with_openmeteo( self, current: CurrentConditions | None, @@ -1191,16 +1166,6 @@ async def _augment_current_with_openmeteo( ) return parsers.merge_current_conditions(current, fallback) - async def _enrich_with_nws_discussion( - self, weather_data: WeatherData, location: Location - ) -> None: - await enrichment.enrich_with_nws_discussion(self, weather_data, location) - - async def _enrich_with_aviation_data( - self, weather_data: WeatherData, location: Location - ) -> None: - await enrichment.enrich_with_aviation_data(self, weather_data, location) - async def get_aviation_weather( self, station_id: str, @@ -1219,28 +1184,6 @@ async def get_aviation_weather( cwsu_id=cwsu_id, ) - async def _enrich_with_visual_crossing_alerts( - self, weather_data: WeatherData, location: Location, skip_notifications: bool = False - ) -> None: - await enrichment.enrich_with_visual_crossing_alerts( - self, weather_data, location, skip_notifications - ) - - async def _enrich_with_visual_crossing_moon_data( - self, weather_data: WeatherData, location: Location - ) -> None: - await enrichment.enrich_with_visual_crossing_moon_data(self, weather_data, location) - - async def _enrich_with_sunrise_sunset( - self, weather_data: WeatherData, location: Location - ) -> None: - await enrichment.enrich_with_sunrise_sunset(self, weather_data, location) - - async def _populate_environmental_metrics( - self, weather_data: WeatherData, location: Location - ) -> None: - await enrichment.populate_environmental_metrics(self, weather_data, location) - async def _enrich_with_visual_crossing_history( self, weather_data: WeatherData, location: Location ) -> None: @@ -1268,14 +1211,6 @@ async def _enrich_with_visual_crossing_history( except Exception as exc: # noqa: BLE001 logger.debug("Failed to fetch history from Visual Crossing: %s", exc) - def _apply_trend_insights(self, weather_data: WeatherData) -> None: - trends.apply_trend_insights( - weather_data, - self.trend_insights_enabled, - self.trend_hours, - include_pressure=self.show_pressure_trend, - ) - def _persist_weather_data(self, location: Location, weather_data: WeatherData) -> None: if not self.offline_cache: return @@ -1287,17 +1222,3 @@ def _persist_weather_data(self, location: Location, weather_data: WeatherData) - self.offline_cache.store(location, weather_data) except Exception as exc: # noqa: BLE001 logger.debug(f"Failed to persist weather data cache: {exc}") - - def _compute_temperature_trend(self, weather_data: WeatherData) -> TrendInsight | None: - return trends.compute_temperature_trend(weather_data, self.trend_hours) - - def _compute_pressure_trend(self, weather_data: WeatherData) -> TrendInsight | None: - return trends.compute_pressure_trend(weather_data, self.trend_hours) - - def _trend_descriptor(self, change: float, *, minor: float, strong: float) -> tuple[str, str]: - return trends.trend_descriptor(change, minor=minor, strong=strong) - - def _period_for_hours_ahead( - self, periods: list[HourlyForecastPeriod] | Sequence[HourlyForecastPeriod], hours_ahead: int - ) -> HourlyForecastPeriod | None: - return trends.period_for_hours_ahead(periods, hours_ahead) diff --git a/tests/test_coverage_fix_pr449.py b/tests/test_coverage_fix_pr449.py index c6f38e07..913de405 100644 --- a/tests/test_coverage_fix_pr449.py +++ b/tests/test_coverage_fix_pr449.py @@ -1,11 +1,12 @@ """ -Surgical coverage tests for PR #449 changed lines. +Surgical coverage tests for PR #449 and PR #456 changed lines. Targets uncovered lines identified by diff-cover: - src/accessiweather/models/alerts.py line 37 (references=None branch) - src/accessiweather/weather_client_nws.py lines 865-868 (client=None branch) - src/accessiweather/weather_client_nws.py lines 1336-1338 (parse_nws_alerts references) - src/accessiweather/weather_client_base.py lines 742-743 (_fetch_nws_cancel_references in auto path) +- src/accessiweather/weather_client_base.py lines 912-924 (_launch_enrichment_tasks auto-mode tasks) """ from __future__ import annotations @@ -104,3 +105,79 @@ def test_parse_nws_alerts_extracts_references(): assert "ref-A" in alerts_obj.alerts[0].references assert "ref-B" in alerts_obj.alerts[0].references assert "ref-C" in alerts_obj.alerts[0].references + + +# --------------------------------------------------------------------------- +# 4. weather_client_base.py lines 912-924: _launch_enrichment_tasks auto-mode +# These lines are inside `if self.data_source == "auto":` and create tasks +# for sunrise_sunset, nws_discussion, vc_alerts, and vc_moon_data. +# --------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_launch_enrichment_tasks_auto_mode_creates_smart_tasks(): + """_launch_enrichment_tasks with data_source='auto' creates all four smart enrichment tasks.""" + import asyncio + + import accessiweather.weather_client_enrichment as enrichment + from accessiweather.models import Location, WeatherData + from accessiweather.weather_client import WeatherClient + + client = WeatherClient(data_source="auto") + location = Location(name="NYC", latitude=40.7128, longitude=-74.0060) + weather_data = WeatherData(location=location) + + async def _noop(*args, **kwargs): + pass + + with ( + patch.object(enrichment, "enrich_with_sunrise_sunset", side_effect=_noop), + patch.object(enrichment, "enrich_with_nws_discussion", side_effect=_noop), + patch.object(enrichment, "enrich_with_visual_crossing_alerts", side_effect=_noop), + patch.object(enrichment, "enrich_with_visual_crossing_moon_data", side_effect=_noop), + patch.object(enrichment, "populate_environmental_metrics", side_effect=_noop), + patch.object(enrichment, "enrich_with_aviation_data", side_effect=_noop), + ): + tasks = client._launch_enrichment_tasks(weather_data, location) + # Cancel tasks to prevent ResourceWarning about pending coroutines + for t in tasks.values(): + t.cancel() + await asyncio.gather(*tasks.values(), return_exceptions=True) + + assert "sunrise_sunset" in tasks + assert "nws_discussion" in tasks + assert "vc_alerts" in tasks + assert "vc_moon_data" in tasks + assert "environmental" in tasks + assert "aviation" in tasks + + +@pytest.mark.asyncio +async def test_launch_enrichment_tasks_non_auto_skips_smart_tasks(): + """_launch_enrichment_tasks with data_source != 'auto' does not create smart enrichment tasks.""" + import asyncio + + import accessiweather.weather_client_enrichment as enrichment + from accessiweather.models import Location, WeatherData + from accessiweather.weather_client import WeatherClient + + client = WeatherClient(data_source="nws") + location = Location(name="NYC", latitude=40.7128, longitude=-74.0060) + weather_data = WeatherData(location=location) + + async def _noop(*args, **kwargs): + pass + + with ( + patch.object(enrichment, "populate_environmental_metrics", side_effect=_noop), + patch.object(enrichment, "enrich_with_aviation_data", side_effect=_noop), + ): + tasks = client._launch_enrichment_tasks(weather_data, location) + for t in tasks.values(): + t.cancel() + await asyncio.gather(*tasks.values(), return_exceptions=True) + + assert "sunrise_sunset" not in tasks + assert "nws_discussion" not in tasks + assert "vc_alerts" not in tasks + assert "vc_moon_data" not in tasks + assert "environmental" in tasks + assert "aviation" in tasks