diff --git a/examples/estimate.py b/examples/estimate.py index 5dd8424..23beb15 100644 --- a/examples/estimate.py +++ b/examples/estimate.py @@ -1,13 +1,14 @@ +"""Example of how to get an estimate from the Forecast.Solar API.""" + import asyncio -import dataclasses -from datetime import datetime, timezone, timedelta -from pprint import pprint +from datetime import UTC, datetime, timedelta +from pprint import pprint # noqa: F401 from forecast_solar import ForecastSolar, ForecastSolarRatelimitError -async def main(): - """Simple function to test the output.""" +async def main() -> None: + """Get an estimate from the Forecast.Solar API.""" async with ForecastSolar( latitude=52.16, longitude=4.47, @@ -22,7 +23,7 @@ async def main(): except ForecastSolarRatelimitError as err: print("Ratelimit reached") print(f"Rate limit resets at {err.reset_at}") - reset_period = err.reset_at - datetime.now(timezone.utc) + reset_period = err.reset_at - datetime.now(UTC) # Strip microseconds as they are not informative reset_period -= timedelta(microseconds=reset_period.microseconds) print(f"That's in {reset_period}") @@ -33,28 +34,34 @@ async def main(): print() print(f"energy_production_today: {estimate.energy_production_today}") print( - f"energy_production_today_remaining: {estimate.energy_production_today_remaining}" + f"energy_production_today_remaining: " + f"{estimate.energy_production_today_remaining}" ) print( f"power_highest_peak_time_today: {estimate.power_highest_peak_time_today}" ) print(f"energy_production_tomorrow: {estimate.energy_production_tomorrow}") print( - f"power_highest_peak_time_tomorrow: {estimate.power_highest_peak_time_tomorrow}" + f"power_highest_peak_time_tomorrow: " + f"{estimate.power_highest_peak_time_tomorrow}" ) print() print(f"power_production_now: {estimate.power_production_now}") print( - f"power_production in 1 hour: {estimate.power_production_at_time(estimate.now() + timedelta(hours=1))}" + f"power_production in 1 hour: " + f"{estimate.power_production_at_time(estimate.now() + timedelta(hours=1))}" ) print( - f"power_production in 6 hours: {estimate.power_production_at_time(estimate.now() + timedelta(hours=6))}" + f"power_production in 6 hours: " + f"{estimate.power_production_at_time(estimate.now() + timedelta(hours=6))}" ) print( - f"power_production in 12 hours: {estimate.power_production_at_time(estimate.now() + timedelta(hours=12))}" + f"power_production in 12 hours: " + f"{estimate.power_production_at_time(estimate.now() + timedelta(hours=12))}" ) print( - f"power_production in 24 hours: {estimate.power_production_at_time(estimate.now() + timedelta(hours=24))}" + f"power_production in 24 hours: " + f"{estimate.power_production_at_time(estimate.now() + timedelta(hours=24))}" ) print() print(f"energy_current_hour: {estimate.energy_current_hour}") diff --git a/examples/ruff.toml b/examples/ruff.toml index 8caec9e..1c9cd0b 100644 --- a/examples/ruff.toml +++ b/examples/ruff.toml @@ -2,5 +2,6 @@ extend = "../pyproject.toml" lint.extend-ignore = [ - "T201", # Allow the use of print() in examples + "T201", # Allow the use of print() in examples + "ERA001", # Allow the use of comments in examples ] diff --git a/src/forecast_solar/__init__.py b/src/forecast_solar/__init__.py index 799f511..58e0e27 100644 --- a/src/forecast_solar/__init__.py +++ b/src/forecast_solar/__init__.py @@ -1,15 +1,16 @@ """Asynchronous Python client for the Forecast.Solar API.""" from .exceptions import ( - ForecastSolarError, - ForecastSolarConnectionError, - ForecastSolarConfigError, ForecastSolarAuthenticationError, + ForecastSolarConfigError, + ForecastSolarConnectionError, + ForecastSolarError, + ForecastSolarRatelimit, ForecastSolarRequestError, ForecastSolarRatelimitError, ) -from .models import Estimate, AccountType, Ratelimit from .forecast_solar import ForecastSolar +from .models import AccountType, Estimate, Ratelimit __all__ = [ "AccountType", diff --git a/src/forecast_solar/forecast_solar.py b/src/forecast_solar/forecast_solar.py index f5c2cdd..052a5a9 100644 --- a/src/forecast_solar/forecast_solar.py +++ b/src/forecast_solar/forecast_solar.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any +from typing import Any, Self from aiodns import DNSResolver from aiodns.error import DNSError @@ -46,8 +46,8 @@ async def _request( self, uri: str, *, - rate_limit=True, - authenticate=True, + rate_limit: bool = True, + authenticate: bool = True, params: dict[str, Any] | None = None, ) -> dict[str, Any]: """Handle a request to the Forecast.Solar API. @@ -56,6 +56,7 @@ async def _request( the Forecast.Solar API. Args: + ---- uri: Request URI, for example, 'estimate' rate_limit: Parse rate limit from response. Set to False for endpoints that are missing rate limiting headers in response. @@ -63,10 +64,12 @@ async def _request( endpoints that do not provide authentication. Returns: + ------- A Python dictionary (JSON decoded) with the response from the Forecast.Solar API. Raises: + ------ ForecastSolarAuthenticationError: If the API key is invalid. ForecastSolarConnectionError: An error occurred while communicating with the Forecast.Solar API. @@ -76,8 +79,8 @@ async def _request( variables used in the request. ForecastSolarRatelimitError: The number of requests has exceeded the rate limit of the Forecast.Solar API. - """ + """ # Forecast.Solar is currently experiencing IPv6 issues. # However, their DNS does return an non-working IPv6 address. # This ensures we use the IPv4 address. @@ -150,12 +153,13 @@ async def _request( return await response.json() async def validate_plane(self) -> bool: - """Validate plane by calling the Forecast.Solar API + """Validate plane by calling the Forecast.Solar API. - Returns: + Returns + ------- True, if plane is valid. - """ + """ await self._request( f"check/{self.latitude}/{self.longitude}" f"/{self.declination}/{self.azimuth}/{self.kwp}", @@ -166,12 +170,13 @@ async def validate_plane(self) -> bool: return True async def validate_api_key(self) -> bool: - """Validate api key by calling the Forecast.Solar API + """Validate api key by calling the Forecast.Solar API. - Returns: + Returns + ------- True, if api key is valid - """ + """ await self._request("info", rate_limit=False) return True @@ -179,8 +184,10 @@ async def validate_api_key(self) -> bool: async def estimate(self) -> Estimate: """Get solar production estimations from the Forecast.Solar API. - Returns: + Returns + ------- A Estimate object, with a estimated production forecast. + """ params = {"time": "iso8601", "damping": str(self.damping)} if self.inverter is not None: @@ -203,18 +210,22 @@ async def close(self) -> None: if self.session and self._close_session: await self.session.close() - async def __aenter__(self) -> ForecastSolar: + async def __aenter__(self) -> Self: """Async enter. - Returns: + Returns + ------- The ForecastSolar object. + """ return self - async def __aexit__(self, *_exc_info) -> None: + async def __aexit__(self, *_exc_info: object) -> None: """Async exit. Args: + ---- _exc_info: Exec type. + """ await self.close() diff --git a/src/forecast_solar/models.py b/src/forecast_solar/models.py index c5b1d80..e947a9b 100644 --- a/src/forecast_solar/models.py +++ b/src/forecast_solar/models.py @@ -2,13 +2,14 @@ from __future__ import annotations -from zoneinfo import ZoneInfo from dataclasses import dataclass -from datetime import datetime, timedelta, date +from datetime import date, datetime, timedelta from enum import Enum -from typing import Any +from typing import TYPE_CHECKING, Any +from zoneinfo import ZoneInfo -from aiohttp import ClientResponse +if TYPE_CHECKING: + from aiohttp import ClientResponse def _timed_value(at: datetime, data: dict[datetime, int]) -> int | None: @@ -26,7 +27,6 @@ def _interval_value_sum( interval_begin: datetime, interval_end: datetime, data: dict[datetime, int] ) -> int: """Return the sum of values in interval.""" - total = 0 for timestamp, wh in data.items(): @@ -54,10 +54,12 @@ class AccountType(str, Enum): class Estimate: """Object holding estimate forecast results from Forecast.Solar. - Attributes: + Attributes + ---------- watts: Estimated solar power output per time period. wh_period: Estimated solar energy production differences per hour. wh_days: Estimated solar energy production per day. + """ watts: dict[datetime, int] @@ -148,6 +150,7 @@ def peak_production_time(self, specific_date: date) -> datetime: ) in self.watts.items(): if watt == value: return timestamp + return None def power_production_at_time(self, time: datetime) -> int: """Return estimated power production at a specific time.""" @@ -168,10 +171,13 @@ def from_dict(cls: type[Estimate], data: dict[str, Any]) -> Estimate: a Estimate object. Args: + ---- data: The estimate response from the Forecast.Solar API. Returns: + ------- An Estimate object. + """ return cls( watts={ diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5d6b3fd..a09ab2a 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -8,8 +8,10 @@ ForecastSolarRatelimitError, ForecastSolarConfigError, ForecastSolarAuthenticationError, - ForecastSolarRequestError, + ForecastSolarConfigError, ForecastSolarConnectionError, + ForecastSolarRatelimit, + ForecastSolarRequestError, ) from . import load_fixtures diff --git a/tests/test_forecast.py b/tests/test_forecast.py index c32744b..8913679 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -1,16 +1,12 @@ """Tests for Forecast.Solar.""" # pylint: disable=protected-access -import asyncio -from unittest.mock import patch import pytest -from aiohttp import ClientError, ClientResponse, ClientSession -from aresponses import Response, ResponsesMockServer +from aresponses import ResponsesMockServer from forecast_solar import ( ForecastSolar, - ForecastSolarConnectionError, ForecastSolarError, ) diff --git a/tests/test_models.py b/tests/test_models.py index 73ea52b..7eb837b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,12 +1,12 @@ """Test the models.""" -import pytest from datetime import datetime + +import pytest from aresponses import ResponsesMockServer from syrupy.assertion import SnapshotAssertion -from forecast_solar import ForecastSolar, Estimate, AccountType - +from forecast_solar import AccountType, Estimate, ForecastSolar from . import load_fixtures