Skip to content

Commit

Permalink
feat!: Rewrite Sunrise to v3.0 and dataclasses
Browse files Browse the repository at this point in the history
  • Loading branch information
ZeroWave022 committed Oct 10, 2023
1 parent 0f3168f commit 65303e4
Showing 1 changed file with 145 additions and 87 deletions.
232 changes: 145 additions & 87 deletions yr_weather/sunrise.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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.
Expand Down

0 comments on commit 65303e4

Please sign in to comment.