fix(prediction): green up CI for the new PV providers (mypy + provider sequence)

- Use urllib.parse.quote instead of requests.utils.quote in the pvnode and
  Solcast providers: the runtime re-export exists, but the requests type stubs
  do not declare it, so the pre-commit mypy hook failed with
  'Module has no attribute "quote"'.
- Mark the force_update keyword in the new provider tests with `# type: ignore`
  — it is consumed by the cache_in_file decorator at runtime; same call
  convention and ignore style as pvforecastakkudoktor.py.
- Add PVForecastPVNode, PVForecastForecastSolar and PVForecastSolcast to the
  expected provider sequence in tests/test_prediction.py (fixture + index
  assertions) — the two sequence tests failed because the new providers were
  registered in prediction.py but missing from the hardcoded expectations.
This commit is contained in:
Christin
2026-07-04 09:23:57 +00:00
parent 6f4a26a045
commit d4dc9fa662
5 changed files with 25 additions and 10 deletions

View File

@@ -22,6 +22,7 @@ Notes:
import re
from typing import Any, Optional
from urllib.parse import quote
import pendulum
import requests
@@ -165,7 +166,7 @@ class PVForecastPVNode(PVForecastProvider):
try:
if site_id:
url = f"{PVNODE_BASE}/forecast/{requests.utils.quote(site_id, safe='')}"
url = f"{PVNODE_BASE}/forecast/{quote(site_id, safe='')}"
response = requests.get(url, headers=headers, params=params, timeout=30)
else:
body = self._inline_body()

View File

@@ -18,6 +18,7 @@ Notes:
import re
from typing import Any, Optional
from urllib.parse import quote
import requests
from loguru import logger
@@ -84,7 +85,7 @@ class PVForecastSolcast(PVForecastProvider):
if not settings.api_key or not settings.site_id:
raise ValueError("PVForecastSolcast requires api_key and site_id")
url = f"{SOLCAST_BASE}/{requests.utils.quote(settings.site_id, safe='')}/forecasts"
url = f"{SOLCAST_BASE}/{quote(settings.site_id, safe='')}/forecasts"
params = {"format": "json", "hours": "72"}
headers = {"Authorization": f"Bearer {settings.api_key}", "Accept": "application/json"}
logger.debug(f"Requesting Solcast forecast: {url}")

View File

@@ -19,7 +19,10 @@ from akkudoktoreos.prediction.prediction import (
PredictionCommonSettings,
)
from akkudoktoreos.prediction.pvforecastakkudoktor import PVForecastAkkudoktor
from akkudoktoreos.prediction.pvforecastforecastsolar import PVForecastForecastSolar
from akkudoktoreos.prediction.pvforecastimport import PVForecastImport
from akkudoktoreos.prediction.pvforecastpvnode import PVForecastPVNode
from akkudoktoreos.prediction.pvforecastsolcast import PVForecastSolcast
from akkudoktoreos.prediction.pvforecastvrm import PVForecastVrm
from akkudoktoreos.prediction.weatherbrightsky import WeatherBrightSky
from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
@@ -49,6 +52,9 @@ def forecast_providers():
LoadImport(),
PVForecastAkkudoktor(),
PVForecastVrm(),
PVForecastPVNode(),
PVForecastForecastSolar(),
PVForecastSolcast(),
PVForecastImport(),
WeatherBrightSky(),
WeatherClearOutside(),
@@ -98,11 +104,14 @@ def test_provider_sequence(prediction):
assert isinstance(prediction.providers[9], LoadImport)
assert isinstance(prediction.providers[10], PVForecastAkkudoktor)
assert isinstance(prediction.providers[11], PVForecastVrm)
assert isinstance(prediction.providers[12], PVForecastImport)
assert isinstance(prediction.providers[13], WeatherBrightSky)
assert isinstance(prediction.providers[14], WeatherClearOutside)
assert isinstance(prediction.providers[15], WeatherOpenMeteo)
assert isinstance(prediction.providers[16], WeatherImport)
assert isinstance(prediction.providers[12], PVForecastPVNode)
assert isinstance(prediction.providers[13], PVForecastForecastSolar)
assert isinstance(prediction.providers[14], PVForecastSolcast)
assert isinstance(prediction.providers[15], PVForecastImport)
assert isinstance(prediction.providers[16], WeatherBrightSky)
assert isinstance(prediction.providers[17], WeatherClearOutside)
assert isinstance(prediction.providers[18], WeatherOpenMeteo)
assert isinstance(prediction.providers[19], WeatherImport)
def test_provider_by_id(prediction, forecast_providers):

View File

@@ -84,7 +84,9 @@ def test_plane_url_converts_azimuth(config_eos):
config=config_eos.load, start_datetime=pendulum.datetime(2025, 1, 1, tz="UTC")
)
with patch("requests.get", return_value=_http({})) as mock_get:
pv._request_forecast(force_update=True)
# force_update is consumed by the cache_in_file decorator at runtime
# (same call convention as pvforecastakkudoktor.py).
pv._request_forecast(force_update=True) # type: ignore
url = mock_get.call_args[0][0]
assert url == "https://api.forecast.solar/secret/estimate/52.5/13.4/25.0/90.0/7.5"
@@ -106,7 +108,7 @@ def test_request_forecast_sums_planes(config_eos):
_http({"2025-01-01 12:00:00": 800.0}),
]
with patch("requests.get", side_effect=responses) as mock_get:
body = pv._request_forecast(force_update=True)
body = pv._request_forecast(force_update=True) # type: ignore
assert mock_get.call_count == 2
assert body["watts"]["2025-01-01 12:00:00"] == 1800.0

View File

@@ -129,7 +129,9 @@ def test_request_forecast_inline_post_when_no_site(config_eos):
pv = PVForecastPVNode(config=config_eos.load, start_datetime=pendulum.datetime(2025, 1, 1, tz="UTC"))
fake = type("R", (), {"raise_for_status": lambda self: None, "json": lambda self: {"values": []}})()
with patch("requests.post", return_value=fake) as mock_post:
pv._request_forecast(force_update=True)
# force_update is consumed by the cache_in_file decorator at runtime
# (same call convention as pvforecastakkudoktor.py).
pv._request_forecast(force_update=True) # type: ignore
url = mock_post.call_args[0][0]
assert url.endswith("/v2/forecast/inline")
body = mock_post.call_args.kwargs["json"]