From 65303e4b6f7132da79b3421b60f9fa9c830f0789 Mon Sep 17 00:00:00 2001 From: ZeroWave022 <36341766+ZeroWave022@users.noreply.github.com> Date: Tue, 10 Oct 2023 19:15:18 +0200 Subject: [PATCH] feat!: Rewrite Sunrise to v3.0 and dataclasses --- yr_weather/sunrise.py | 232 ++++++++++++++++++++++++++---------------- 1 file changed, 145 insertions(+), 87 deletions(-) diff --git a/yr_weather/sunrise.py b/yr_weather/sunrise.py index d99aedb..8d695ba 100644 --- a/yr_weather/sunrise.py +++ b/yr_weather/sunrise.py @@ -1,63 +1,125 @@ """A module with classes for the Sunrise API.""" from datetime import datetime -from typing import Optional, get_args +from dataclasses import dataclass +from typing import Optional, Literal, List import requests from .client import APIClient -from .types.sunrise import SunriseData, DetailLiteral + +@dataclass +class EventsGeometry: + """A dataclass holding coordinates.""" + + coordinates: List[float] + type: Literal["Point"] + + +@dataclass +class CommonEventsData: + """A dataclass with common event data for both sun and moon events.""" + + type: Optional[str] = None + copyright: Optional[str] = None + license_url: Optional[str] = None + geometry: Optional[EventsGeometry] = None + interval: Optional[List[str]] = None + + def __post_init__(self): + if self.geometry: + self.geometry = EventsGeometry(**self.geometry) + + +@dataclass +class TimeWithAzimuth: + """A dataclass with event time data with azimuth.""" + + time: str + azimuth: float + + +@dataclass +class TimeWithElevation: + """A dataclass with event data with disc centre elevation.""" + + time: str + disc_centre_elevation: float + visible: bool + + +class SunEvents(CommonEventsData): + """A class with sun event data.""" + + def __init__(self, data: dict): + super().__init__( + type=data["type"], + copyright=data["copyright"], + license_url=data["licenseURL"], + geometry=data["geometry"], + interval=data["when"]["interval"], + ) + + props = data["properties"] + + self.body: Literal["Sun"] = props["body"] + self.sunrise = TimeWithAzimuth(**props["sunrise"]) + self.sunset = TimeWithAzimuth(**props["sunset"]) + self.solarnoon = TimeWithElevation(**props["solarnoon"]) + self.solarmidnight = TimeWithElevation(**props["solarmidnight"]) + + +class MoonEvents(CommonEventsData): + """A class with moon event data.""" + + def __init__(self, data: dict): + super().__init__( + type=data["type"], + copyright=data["copyright"], + license_url=data["licenseURL"], + geometry=data["geometry"], + interval=data["when"]["interval"], + ) + + props = data["properties"] + + self.body: Literal["Moon"] = props["body"] + self.moonrise = TimeWithAzimuth(**props["moonrise"]) + self.moonset = TimeWithAzimuth(**props["moonset"]) + self.high_moon = TimeWithElevation(**props["high_moon"]) + self.low_moon = TimeWithElevation(**props["low_moon"]) + self.moonphase: float = props["moonphase"] class Sunrise(APIClient): """A client for interacting with the Yr Sunrise API.""" - def __init__(self, headers=None, use_cache=True) -> None: + def __init__(self, headers, use_cache=True) -> None: super().__init__(headers, use_cache) - self._base_url += "sunrise/2.0/" + self._base_url += "sunrise/3.0/" - def get_sunrise( - self, - date: str, - lat: float, - lon: float, - offset: str, - days_forward: Optional[int] = None, - height: Optional[float] = None, - ) -> SunriseData: - """Get sunrise data. - - For more information, please see: https://api.met.no/weatherapi/sunrise/2.0/documentation - - Parameters - ---------- - date: :class:`str` - A date formatted in ISO 8601 format, like so: `YYYY-MM-DD`. - lat: :class:`float` | :class:`int` - The latitude of the location. - lon: :class:`float` | :class:`int` - The longitude of the location. - offset: :class:`str` - The timezone offset, given in the following format: `+HH:MM` or `-HH:MM`. - days_forward: Optional[:class:`int`] - Optional: The number of future days which should be included. Default is :class:`None`. - height: Optional[:class:`float` | :class:`int`] - Optional: The altitude above the ellipsoid in kilometers (km). Default is :class:`None`. + def _get_events(self, event_type: str, **kwargs) -> dict: + date = kwargs.get("date") + lat = kwargs.get("lat") + lon = kwargs.get("lon") + offset = kwargs.get("offset") - Returns - ------- - :class:`.SunriseData` - A typed dict containing sunrise data. - """ # Ensure correct variable types. if not isinstance(date, str): - raise ValueError("Type of 'data' must be str.") + raise ValueError("Type of 'date' must be str.") if not isinstance(lat, (int, float)) or not isinstance(lon, (int, float)): raise ValueError("Type of 'lat' and 'lon' must be int or float.") - if not isinstance(offset, str): - raise ValueError("Type of 'offset' must be str.") + if offset: + if not isinstance(offset, str): + raise ValueError("Type of 'offset' must be str.") + + # Ensure offset is valid. + if not self._ensure_valid_offset(offset): + raise ValueError( + "The 'offset' parameter is not a valid timezone offset." + ) # Check if the date provided is valid. try: @@ -66,83 +128,79 @@ def get_sunrise( except Exception as exc: raise ValueError("The 'date' parameter must be a valid date.") from exc - # Ensure offset is valid. - if not self._ensure_valid_offset(offset): - raise ValueError("The 'offset' parameter is not a valid timezone offset.") - - # Set up URL and add optional parameters if present and valid. - url = self._base_url + f".json?date={date}&lat={lat}&lon={lon}&offset={offset}" - - if isinstance(days_forward, int): - url += f"&days={days_forward}" - elif days_forward is not None: - raise ValueError("Type of 'days_forward' must be int.") + url = self._base_url + event_type - if isinstance(height, (int, float)): - url += f"&height={height}" - elif height is not None: - raise ValueError("Type of 'height' must be int or float.") - - request = self.session.get(url) + request = self.session.get( + url, + params={"date": date, "lat": str(lat), "lon": str(lon), "offset": offset}, + ) if not request.ok: raise requests.HTTPError( f"Unsuccessful response received: {request.status_code} {request.reason}." ) - sunrise_data: SunriseData = request.json() - - return sunrise_data + return request.json() - def get_detail( + def get_sun_events( self, - detail: DetailLiteral, date: str, lat: float, lon: float, - offset: str, - height: Optional[float] = None, - ): - """Get data about the specified event or detail. + offset: Optional[str] = None, + ) -> SunEvents: + """Get sun events data (sunrise, sunset, etc). - This will get the newest sunrise data, and return the event/detail dict, if available. - For more information, please see: https://api.met.no/weatherapi/sunrise/2.0/documentation + For more information, please see: https://api.met.no/weatherapi/sunrise/3.0/documentation Parameters ---------- - detail: :data:`.DetailLiteral` - The detail/event to get data for. See :data:`.DetailLiteral` for valid values. date: :class:`str` A date formatted in ISO 8601 format, like so: `YYYY-MM-DD`. lat: :class:`float` | :class:`int` The latitude of the location. lon: :class:`float` | :class:`int` The longitude of the location. - offset: :class:`str` - The timezone offset, given in the following format: `+/-HH:MM`. - height: Optional[:class:`float` | :class:`int`] - The altitude above the ellipsoid in kilometers (km). + offset: Optional[:class:`str`] + The timezone offset, given in the following format: `+HH:MM` or `-HH:MM`. Returns ------- - :class:`dict` - Details about the chosen event or detail. + :class:`.SunEvents` """ - literals = get_args(DetailLiteral) + data = self._get_events("sun", date=date, lat=lat, lon=lon, offset=offset) - if detail not in literals: - raise ValueError( - f"The 'detail' parameter must be one of the possible DetailLiterals: {literals}" - ) + return SunEvents(data) + + def get_moon_events( + self, + date: str, + lat: float, + lon: float, + offset: Optional[str] = None, + ) -> MoonEvents: + """Get moon events data (moonrise, moonset, etc). - data = self.get_sunrise(date, lat, lon, offset, height=height) + For more information, please see: https://api.met.no/weatherapi/sunrise/3.0/documentation - try: - return data["location"]["time"][0][detail] - except Exception as exc: - raise RuntimeError( - "This detail is not available for this combination of date and location." - ) from exc + Parameters + ---------- + date: :class:`str` + A date formatted in ISO 8601 format, like so: `YYYY-MM-DD`. + lat: :class:`float` | :class:`int` + The latitude of the location. + lon: :class:`float` | :class:`int` + The longitude of the location. + offset: Optional[:class:`str`] + The timezone offset, given in the following format: `+HH:MM` or `-HH:MM`. + + Returns + ------- + :class:`.MoonEvents` + """ + data = self._get_events("moon", date=date, lat=lat, lon=lon, offset=offset) + + return MoonEvents(data) def _ensure_valid_offset(self, offset: str) -> bool: """Ensures that a valid offset is given.