diff --git a/.flake8 b/.flake8 index 0f464316..4fb1b95c 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] select = B,B9,C,D,DAR,E,F,N,RST,S,W ignore = E203,E501,RST201,RST203,RST301,W503 -max-line-length = 80 +max-line-length = 100 max-complexity = 10 docstring-convention = google per-file-ignores = tests/*:S101 diff --git a/pyproject.toml b/pyproject.toml index 5cae26f1..2f3d4439 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "meteofrance-api" -version = "1.1.0" +version = "1.2.0" description = "Python client for Météo-France API." authors = ["oncleben31 ", "quentame ", "HACF "] license = "MIT" diff --git a/src/meteofrance_api/client.py b/src/meteofrance_api/client.py index 7f3249a2..3f6d1ec8 100644 --- a/src/meteofrance_api/client.py +++ b/src/meteofrance_api/client.py @@ -7,9 +7,9 @@ from .const import METEOFRANCE_API_TOKEN from .const import METEOFRANCE_API_URL from .model import CurrentPhenomenons -from .model import Observation from .model import Forecast from .model import Full +from .model import Observation from .model import PictureOfTheDay from .model import Place from .model import Rain @@ -74,10 +74,9 @@ def search_places( resp = self.session.request("get", "places", params=params) return [Place(place_data) for place_data in resp.json()] - # # Observation - # + # def get_observation( self, latitude: float, @@ -102,10 +101,10 @@ def get_observation( resp = self.session.request( "get", "v2/observation", - params={"lat": latitude, "lon": longitude, "lang": language}) + params={"lat": latitude, "lon": longitude, "lang": language}, + ) return Observation(resp.json()) - def get_observation_for_place( self, place: Place, @@ -125,7 +124,6 @@ def get_observation_for_place( """ return self.get_observation(place.latitude, place.longitude, language) - # # Forecast # diff --git a/src/meteofrance_api/model/__init__.py b/src/meteofrance_api/model/__init__.py index c337192a..3b2f9b67 100644 --- a/src/meteofrance_api/model/__init__.py +++ b/src/meteofrance_api/model/__init__.py @@ -7,4 +7,12 @@ from .warning import CurrentPhenomenons from .warning import Full -__all__ = ["Forecast", "Observation", "Place", "PictureOfTheDay", "Rain", "CurrentPhenomenons", "Full"] +__all__ = [ + "Forecast", + "Observation", + "Place", + "PictureOfTheDay", + "Rain", + "CurrentPhenomenons", + "Full", +] diff --git a/src/meteofrance_api/model/observation.py b/src/meteofrance_api/model/observation.py index d766731f..77e8df3b 100644 --- a/src/meteofrance_api/model/observation.py +++ b/src/meteofrance_api/model/observation.py @@ -2,110 +2,131 @@ """Weather observation Python model for the Météo-France REST API.""" import sys from datetime import datetime +from typing import Any +from typing import Dict +from typing import Optional if sys.version_info >= (3, 8): - from typing import TypedDict # pylint: disable=no-name-in-module + from typing import TypedDict # pylint: disable=no-name-in-module else: - from typing_extensions import TypedDict + from typing_extensions import TypedDict -class ObservationDataPropertiesGridded(TypedDict, total=True): - time: str - T: float - wind_speed: float - wind_direction: int - wind_icon: str - weather_icon: str - weather_description: str +class ObservationDataPropertiesGridded(TypedDict, total=False): + """Data structure of the observation gridded properties object from the REST API.""" + + time: str + T: float + wind_speed: float + wind_direction: int + wind_icon: str + weather_icon: str + weather_description: str class ObservationDataProperties(TypedDict, total=False): - timezone: str - gridded: ObservationDataPropertiesGridded + """Data structure of the observation properties object from the REST API.""" + + timezone: str + gridded: ObservationDataPropertiesGridded class ObservationData(TypedDict, total=False): - """Describing the data structure of the observation object returned by the REST API.""" - - properties: ObservationDataProperties + """Data structure of the observation object from the REST API.""" + + update_time: str + geometry: Dict[str, Any] + type: str + properties: ObservationDataProperties class Observation: - """Class to access the results of an `observation` API request. - - Attributes: - timezone: The observation timezone - time: The time at which the observation was made - temperature: The observed temperature (°C) - wind_speed: The observed wind speed (km/h) - wind_direction: The observed wind direction (°) - wind_icon: An icon ID illustrating the observed wind direction - weather_icon: An icon ID illustrating the observed weather condition - weather_description: A description of the observed weather condition - """ - - def __init__(self, raw_data: ObservationData) -> None: - """Initialize an Observation object. - - Args: - raw_data: A dictionary representing the JSON response from 'observation' REST - API request. The structure is described by the ObservationData class. - """ - self.properties = raw_data.get('properties', dict()) - - @property - def timezone(self) -> str: - """Returns the observation timezone""" - return self.properties.get('timezone') - - @property - def _gridded(self) -> ObservationDataPropertiesGridded: - """Returns the observation gridded properties""" - return self.properties.get('gridded', dict()) - - @property - def time_as_string(self) -> str: - """Returns the time at which the observation was made""" - return self._gridded.get('time') - - @property - def time_as_datetime(self) -> str: - """Returns the time at which the observation was made""" - time = self.time_as_string - return None if time is None else datetime.strptime(time, '%Y-%m-%dT%H:%M:%S.%f%z') - - @property - def temperature(self) -> float: - """Returns the observed temperature (°C)""" - return self._gridded.get('T') - - @property - def wind_speed(self) -> float: - """Returns the observed wind speed (km/h)""" - return self._gridded.get('wind_speed') - - @property - def wind_direction(self) -> int: - """Returns the observed wind direction (°)""" - return self._gridded.get('wind_direction') - - @property - def wind_icon(self) -> str: - """Returns an icon ID illustrating the observed wind direction""" - return self._gridded.get('wind_icon') - - @property - def weather_icon(self) -> str: - """Returns an icon ID illustrating the observed weather condition""" - return self._gridded.get('weather_icon') - - @property - def weather_description(self) -> str: - """Returns a description of the observed weather condition""" - return self._gridded.get('weather_description') - - def __repr__(self) -> str: - """Returns a stringified version of the object""" - return f"Observation(timezone={self.timezone}, time={self.time}, temperature={self.temperature}°C, "\ - f"wind_speed={self.wind_speed} km/h, wind_direction={self.wind_direction}°, wind_icon={self.wind_icon}, "\ - f"weather_icon={self.weather_icon}, weather_description={self.weather_description}" + """Class to access the results of an `observation` API request. + + Attributes: + timezone: The observation timezone + time: The time at which the observation was made + temperature: The observed temperature (°C) + wind_speed: The observed wind speed (km/h) + wind_direction: The observed wind direction (°) + wind_icon: An icon ID illustrating the observed wind direction + weather_icon: An icon ID illustrating the observed weather condition + weather_description: A description of the observed weather condition + """ + + def __init__(self, raw_data: ObservationData) -> None: + """Initialize an Observation object. + + Args: + raw_data: A dictionary representing the JSON response from 'observation' REST + API request. The structure is described by the ObservationData class. + """ + self.properties = raw_data.get("properties", {}) + + @property + def timezone(self) -> Optional[str]: + """Returns the observation timezone.""" + return self.properties.get("timezone") + + @property + def _gridded(self) -> ObservationDataPropertiesGridded: + """Returns the observation gridded properties.""" + return self.properties.get("gridded", {}) + + @property + def time_as_string(self) -> Optional[str]: + """Returns the time at which the observation was made.""" + return self._gridded.get("time") + + @property + def time_as_datetime(self) -> Optional[datetime]: + """Returns the time at which the observation was made.""" + time = self.time_as_string + return ( + None if time is None else datetime.strptime(time, "%Y-%m-%dT%H:%M:%S.%f%z") + ) + + @property + def temperature(self) -> Optional[float]: + """Returns the observed temp (°C).""" + return self._gridded.get("T") + + @property + def wind_speed(self) -> Optional[float]: + """Returns the observed wind speed (km/h).""" + return self._gridded.get("wind_speed") + + @property + def wind_direction(self) -> Optional[int]: + """Returns the observed wind direction (°).""" + return self._gridded.get("wind_direction") + + @property + def wind_icon(self) -> Optional[str]: + """Returns an icon ID illustrating the observed wind direction.""" + return self._gridded.get("wind_icon") + + @property + def weather_icon(self) -> Optional[str]: + """Returns an icon ID illustrating the observed weather condition.""" + return self._gridded.get("weather_icon") + + @property + def weather_description(self) -> Optional[str]: + """Returns a description of the observed weather condition.""" + return self._gridded.get("weather_description") + + def __repr__(self) -> str: + """Returns a stringified version of the object.""" + return ( + f"Observation(" + f" timezone={self.timezone}," + f" time={self.time_as_string}," + f" temperature={self.temperature}°C," + f" wind_speed={self.wind_speed} km/h," + f" wind_direction={self.wind_direction}°," + f" wind_icon={self.wind_icon}," + f" weather_icon={self.weather_icon}," + f" weather_description={self.weather_description}" + ")" + ) diff --git a/tests/test_observation.py b/tests/test_observation.py index 9121983c..36e085ba 100644 --- a/tests/test_observation.py +++ b/tests/test_observation.py @@ -1,37 +1,47 @@ # coding: utf-8 """Tests Météo-France module. Observation class.""" -import time -from datetime import datetime, timezone, timedelta +from datetime import datetime +from datetime import timedelta +from datetime import timezone from .const import MOUNTAIN_CITY from meteofrance_api import MeteoFranceClient +from meteofrance_api.model import Observation from meteofrance_api.model import Place -def assert_types(observation) -> None: - """Check observation types""" +def assert_types(observation: Observation) -> None: + """Check observation types.""" assert type(observation.timezone) == str assert type(observation.time_as_string) == str assert type(observation.time_as_datetime) == datetime - assert type(observation.temperature) == float - assert type(observation.wind_speed) == float + assert ( + type(observation.temperature) == float or type(observation.temperature) == int + ) + assert type(observation.wind_speed) == float or type(observation.wind_speed) == int assert type(observation.wind_direction) == int assert type(observation.wind_icon) == str assert type(observation.weather_icon) == str assert type(observation.weather_description) == str -def assert_datetime(observation) -> None: +def assert_datetime(observation: Observation) -> None: """Check observation time is before now but after now - 1h.""" now = datetime.now(timezone.utc) - assert now - timedelta(hours=1) < observation.time_as_datetime < now + assert ( + True + if observation.time_as_datetime is None + else now - timedelta(hours=1) < observation.time_as_datetime < now + ) def test_observation_france() -> None: """Test weather observation results from API (valid result, from lat/lon).""" client = MeteoFranceClient() observation = client.get_observation(latitude=48.8075, longitude=2.24028) - + + assert str(observation) + assert_types(observation) assert_datetime(observation) @@ -41,6 +51,8 @@ def test_observation_world() -> None: client = MeteoFranceClient() observation = client.get_observation(latitude=45.5016889, longitude=73.567256) + assert str(observation) + assert observation.timezone is None assert observation.time_as_string is None assert observation.time_as_datetime is None @@ -56,6 +68,8 @@ def test_observation_place() -> None: """Test weather observation results from API (valid result, from place).""" client = MeteoFranceClient() observation = client.get_observation_for_place(place=Place(MOUNTAIN_CITY)) - + + assert str(observation) + assert_types(observation) assert_datetime(observation)