From cca943c0fa786518ee9ec53cc5c1ed715a02dafa Mon Sep 17 00:00:00 2001 From: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:51:09 +0800 Subject: [PATCH 1/3] refactor: simplify weather provider dispatch --- tests/test_cli.py | 6 ++---- weather_briefing/composition/providers.py | 17 ++++------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 10b673a5..f2a7ad8e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -39,7 +39,6 @@ ) from weather_briefing.composition.providers import ( PUBLISHER_BUILDERS, - WEATHER_PROVIDER_BUILDERS, _build_jma, _build_nea, _build_open_meteo, @@ -51,7 +50,7 @@ from weather_briefing.composition.providers import weather_provider_metadata as _weather_provider_metadata from weather_briefing.config import ConfigurationError, Settings from weather_briefing.models import LocationSpec, ResolvedLocation -from weather_briefing.registries import PublisherName, WeatherProviderName +from weather_briefing.registries import PublisherName from weather_briefing.state import SQLiteRuntimeDiagnostics, SQLiteStateStore from weather_briefing.weather import QWeatherProvider @@ -1490,8 +1489,7 @@ def test_parse_run_time_returns_now_when_value_is_none(monkeypatch) -> None: assert result.timezone_name == "Asia/Shanghai" -def test_runtime_provider_builders_cover_declared_configuration_names() -> None: - assert set(WEATHER_PROVIDER_BUILDERS) == set(WeatherProviderName) +def test_publisher_builders_cover_declared_configuration_names() -> None: assert set(PUBLISHER_BUILDERS) == set(PublisherName) diff --git a/weather_briefing/composition/providers.py b/weather_briefing/composition/providers.py index 491b01cc..6c2be3eb 100644 --- a/weather_briefing/composition/providers.py +++ b/weather_briefing/composition/providers.py @@ -258,16 +258,15 @@ def build_weather_provider( jma_office_code: str | None = None, ) -> WeatherContextProvider: """Build one configured weather provider adapter.""" - builder = WEATHER_PROVIDER_BUILDERS.get(name) - if builder is None: - raise ValueError(f"Unsupported weather provider: {name}") if name == WeatherProviderName.QWEATHER: return _build_qweather(settings, client, output_language=output_language) + if name == WeatherProviderName.OPEN_METEO: + return _build_open_meteo(settings, client) if name == WeatherProviderName.NEA_SINGAPORE: - return NEASingaporeNowcastProvider(client, api_key=settings.nea_api_key) + return _build_nea(settings, client) if name == WeatherProviderName.JMA_JAPAN: return _build_jma(settings, client, office_code=jma_office_code) - return builder(settings, client) + raise ValueError(f"Unsupported weather provider: {name}") def _build_qweather( @@ -319,11 +318,3 @@ def aqicn_provider(settings: Settings, client: httpx.AsyncClient) -> AirQualityP if not settings.aqicn_api_token: return None return AQICNProvider(client, token=settings.aqicn_api_token) - - -WEATHER_PROVIDER_BUILDERS: dict[str, Callable[[Settings, httpx.AsyncClient], WeatherContextProvider]] = { - WeatherProviderName.QWEATHER: _build_qweather, - WeatherProviderName.OPEN_METEO: _build_open_meteo, - WeatherProviderName.NEA_SINGAPORE: _build_nea, - WeatherProviderName.JMA_JAPAN: _build_jma, -} From 30d23a0319f6d77fa8d16d2969bbd34420ca7adf Mon Sep 17 00:00:00 2001 From: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:00:40 +0800 Subject: [PATCH 2/3] test: cover weather provider dispatch --- tests/test_cli.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index f2a7ad8e..b88bcbae 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -50,7 +50,7 @@ from weather_briefing.composition.providers import weather_provider_metadata as _weather_provider_metadata from weather_briefing.config import ConfigurationError, Settings from weather_briefing.models import LocationSpec, ResolvedLocation -from weather_briefing.registries import PublisherName +from weather_briefing.registries import PublisherName, WeatherProviderName from weather_briefing.state import SQLiteRuntimeDiagnostics, SQLiteStateStore from weather_briefing.weather import QWeatherProvider @@ -1367,6 +1367,23 @@ async def test_build_weather_provider_unsupported(async_client: httpx.AsyncClien _build_weather_provider("unknown", settings, async_client) +@pytest.mark.parametrize("name", tuple(WeatherProviderName)) +async def test_build_weather_provider_dispatch_is_exhaustive( + async_client: httpx.AsyncClient, + name: WeatherProviderName, +) -> None: + settings = _make_fake_settings( + qweather_project_id="project", + qweather_credential_id="credential", + qweather_private_key=base64.b64encode(b"fake-private-key-content").decode(), + qweather_base_url="https://qweather.example.invalid", + ) + + provider = _build_weather_provider(name, settings, async_client, jma_office_code="130000") + + assert provider is not None + + async def test_build_open_meteo_returns_provider(async_client: httpx.AsyncClient) -> None: settings = _make_fake_settings() provider = _build_open_meteo(settings, async_client) From ca07e2a979e068576a7cfd6a788514193d8a0bb1 Mon Sep 17 00:00:00 2001 From: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:06:32 +0800 Subject: [PATCH 3/3] test: colocate the provider dispatch invariant --- tests/test_cli.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index b88bcbae..d73cfbe0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1367,23 +1367,6 @@ async def test_build_weather_provider_unsupported(async_client: httpx.AsyncClien _build_weather_provider("unknown", settings, async_client) -@pytest.mark.parametrize("name", tuple(WeatherProviderName)) -async def test_build_weather_provider_dispatch_is_exhaustive( - async_client: httpx.AsyncClient, - name: WeatherProviderName, -) -> None: - settings = _make_fake_settings( - qweather_project_id="project", - qweather_credential_id="credential", - qweather_private_key=base64.b64encode(b"fake-private-key-content").decode(), - qweather_base_url="https://qweather.example.invalid", - ) - - provider = _build_weather_provider(name, settings, async_client, jma_office_code="130000") - - assert provider is not None - - async def test_build_open_meteo_returns_provider(async_client: httpx.AsyncClient) -> None: settings = _make_fake_settings() provider = _build_open_meteo(settings, async_client) @@ -1506,6 +1489,23 @@ def test_parse_run_time_returns_now_when_value_is_none(monkeypatch) -> None: assert result.timezone_name == "Asia/Shanghai" +@pytest.mark.parametrize("name", tuple(WeatherProviderName)) +async def test_build_weather_provider_dispatch_is_exhaustive( + async_client: httpx.AsyncClient, + name: WeatherProviderName, +) -> None: + settings = _make_fake_settings( + qweather_project_id="project", + qweather_credential_id="credential", + qweather_private_key=base64.b64encode(b"fake-private-key-content").decode(), + qweather_base_url="https://qweather.example.invalid", + ) + + provider = _build_weather_provider(name, settings, async_client, jma_office_code="130000") + + assert provider is not None + + def test_publisher_builders_cover_declared_configuration_names() -> None: assert set(PUBLISHER_BUILDERS) == set(PublisherName)