Skip to content

Commit

Permalink
Refactoring. Bump version to 0.0.11. (#29)
Browse files Browse the repository at this point in the history
* Refactoring: remove code duplication (#25)

* Use sync methods from async methods

* Simplify start and end time handling

* Bump setup.py dependencies and fix pylint errors (#26)

* Bump requests version in setup.py

* Add pylint

* Fix pylint errors

* Move utility functions to where they are used (#27)

* Bump version (#28)
  • Loading branch information
saaste authored Mar 4, 2020
1 parent e3e224c commit a4712b1
Show file tree
Hide file tree
Showing 11 changed files with 764 additions and 156 deletions.
582 changes: 582 additions & 0 deletions .pylintrc

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ test: ## Run tests
@pytest
@flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude venv
@flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --exclude venv
@pylint fmi_weather_client
@pylint fmi_weather_client/parsers

clean: ## Clean build and dist directories
@rm -rf ./build ./dist ./fmi_weather_client.egg-info
Expand Down
18 changes: 9 additions & 9 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from fmi_weather_client.models import Forecast, Weather, WeatherData


def print_weather(observation: Weather):
print(observation.place)
print("Location: %s, %s" % (observation.lat, observation.lon))
print_weather_data(observation.data)
def print_weather(weather: Weather):
print(weather.place)
print("Location: %s, %s" % (weather.lat, weather.lon))
print_weather_data(weather.data)
print(" ")


Expand All @@ -18,11 +18,11 @@ def print_forecast(station_forecast: Forecast):


def print_weather_data(weather: WeatherData):
print(" Timestamp: %s" % weather.time)
print(" Temperature: %s" % weather.temperature)
print(" Humidity: %s" % weather.humidity)
print(" Wind speed: %s" % weather.wind_speed)
print(" Cloud cover: %s" % weather.cloud_cover)
print(f" Timestamp: {weather.time}")
print(f" Temperature: {weather.temperature}")
print(f" Humidity: {weather.humidity}")
print(f" Wind speed: {weather.wind_speed}")
print(f" Cloud cover: {weather.cloud_cover}")


weather1 = fmi_weather_client.weather_by_coordinates(60.170998, 24.941325)
Expand Down
16 changes: 4 additions & 12 deletions fmi_weather_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ async def async_weather_by_coordinates(lat: float, lon: float) -> Weather:
:return: Latest weather information
"""
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, http.request_weather_by_coordinates, lat, lon)
forecast = forecast_parser.parse_forecast(response)
weather_state = forecast.forecasts[-1]
return Weather(forecast.place, forecast.lat, forecast.lon, weather_state)
return await loop.run_in_executor(None, weather_by_coordinates, lat, lon)


def weather_by_place_name(name: str) -> Weather:
Expand All @@ -55,10 +52,7 @@ async def async_weather_by_place_name(name: str) -> Weather:
:return: Latest weather information
"""
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, http.request_weather_by_place, name)
forecast = forecast_parser.parse_forecast(response)
weather_state = forecast.forecasts[-1]
return Weather(forecast.place, forecast.lat, forecast.lon, weather_state)
return await loop.run_in_executor(None, weather_by_place_name, name)


def forecast_by_place_name(name: str, timestep_hours: int = 24):
Expand All @@ -80,8 +74,7 @@ async def async_forecast_by_place_name(name: str, timestep_hours: int = 24):
:return: Latest forecast
"""
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, http.request_forecast_by_place, name, timestep_hours)
return forecast_parser.parse_forecast(response)
return await loop.run_in_executor(None, forecast_by_place_name, name, timestep_hours)


def forecast_by_coordinates(lat: float, lon: float, timestep_hours: int = 24):
Expand All @@ -105,5 +98,4 @@ async def async_forecast_by_coordinates(lat: float, lon: float, timestep_hours:
:return: Latest forecast
"""
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, http.request_forecast_by_coordinates, lat, lon, timestep_hours)
return forecast_parser.parse_forecast(response)
return await loop.run_in_executor(None, forecast_by_coordinates, lat, lon, timestep_hours)
4 changes: 2 additions & 2 deletions fmi_weather_client/errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class NoDataAvailableError(Exception):
pass
"""Represents data not available error"""


class ServiceError(Exception):
pass
"""Represents unknown FMI service error"""
45 changes: 25 additions & 20 deletions fmi_weather_client/http.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Any, Dict, Optional

import requests
Expand All @@ -9,6 +10,12 @@
_LOGGER = logging.getLogger(__name__)


class RequestType(Enum):
"""Possible request types"""
WEATHER = 0
FORECAST = 1


def request_weather_by_coordinates(lat: float, lon: float) -> str:
"""
Get the latest weather information by coordinates.
Expand All @@ -17,9 +24,7 @@ def request_weather_by_coordinates(lat: float, lon: float) -> str:
:param lon: Longitude (e.g. 62.39758)
:return: Latest weather information
"""
end_time = datetime.utcnow().replace(tzinfo=timezone.utc)
start_time = end_time - timedelta(minutes=10)
params = _create_params(start_time, end_time, 10, lat=lat, lon=lon)
params = _create_params(RequestType.WEATHER, 10, lat=lat, lon=lon)
return _send_request(params)


Expand All @@ -30,9 +35,7 @@ def request_weather_by_place(place: str) -> str:
:param place: Place name (e.g. Kaisaniemi, Helsinki)
:return: Latest weather information
"""
end_time = datetime.utcnow().replace(tzinfo=timezone.utc)
start_time = end_time - timedelta(minutes=10)
params = _create_params(start_time, end_time, 10, place=place)
params = _create_params(RequestType.WEATHER, 10, place=place)
return _send_request(params)


Expand All @@ -45,10 +48,8 @@ def request_forecast_by_coordinates(lat: float, lon: float, timestep_hours: int
:param timestep_hours: Forecast steps in hours
:return: Forecast response
"""
start_time = datetime.utcnow().replace(tzinfo=timezone.utc)
end_time = start_time + timedelta(days=4)
timestep = timestep_hours * 60
params = _create_params(start_time, end_time, timestep, lat=lat, lon=lon)
timestep_minutes = timestep_hours * 60
params = _create_params(RequestType.FORECAST, timestep_minutes, lat=lat, lon=lon)
return _send_request(params)


Expand All @@ -60,23 +61,18 @@ def request_forecast_by_place(place: str, timestep_hours: int = 24) -> str:
:param timestep_hours: Forecast steps in hours
:return: Forecast response
"""
start_time = datetime.utcnow().replace(tzinfo=timezone.utc)
end_time = start_time + timedelta(days=4)
timestep = timestep_hours * 60
params = _create_params(start_time, end_time, timestep, place=place)
timestep_minutes = timestep_hours * 60
params = _create_params(RequestType.FORECAST, timestep_minutes, place=place)
return _send_request(params)


def _create_params(start_time: datetime,
end_time: datetime,
def _create_params(request_type: RequestType,
timestep_minutes: int,
place: Optional[str] = None,
lat: Optional[float] = None,
lon: Optional[float] = None) -> Dict[str, Any]:
"""
Create query parameters
:param start_time: Start datetime
:param end_time: End datetime
:param timestep_minutes: Timestamp minutes
:param place: Place name
:param lat: Latitude
Expand All @@ -87,6 +83,15 @@ def _create_params(start_time: datetime,
if place is None and lat is None and lon is None:
raise Exception("Missing location parameter")

if request_type is RequestType.WEATHER:
end_time = datetime.utcnow().replace(tzinfo=timezone.utc)
start_time = end_time - timedelta(minutes=10)
elif request_type is RequestType.FORECAST:
start_time = datetime.utcnow().replace(tzinfo=timezone.utc)
end_time = start_time + timedelta(days=4)
else:
raise Exception(f"Invalid request_type {request_type}")

params = {
'service': 'WFS',
'version': '2.0.0',
Expand Down Expand Up @@ -114,11 +119,11 @@ def _send_request(params: Dict[str, Any]) -> str:
"""
url = 'http://opendata.fmi.fi/wfs'

_LOGGER.debug(f"Sending GET to {url} with parameters: {params}")
_LOGGER.debug("Sending GET to %s with parameters: %s", url, params)
response = requests.get(url, params=params)

if response.status_code >= 500:
raise ServiceError("Invalid FMI service response", {'status_code': response.status_code, 'body': response.text})

_LOGGER.debug(f"Received a response from FMI in {response.elapsed.microseconds / 1000} ms", )
_LOGGER.debug("Received a response from FMI in %s ms", response.elapsed.microseconds / 1000)
return response.text
143 changes: 65 additions & 78 deletions fmi_weather_client/models.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,78 @@
from datetime import datetime
from typing import Dict, List, Optional
from typing import List, Optional, NamedTuple


class FMIPlace:
def __init__(self, name: str, lat: float, lon: float):
self.name: str = name
self.lat: float = lat
self.lon: float = lon
class FMIPlace(NamedTuple):
"""Represent a place in FMI response"""
name: str
lat: float
lon: float

def __str__(self):
return f"{self.name} ({self.lat}, {self.lon})"


class Value:
def __init__(self, value: Optional[float], unit: str):
self.value: Optional[float] = value
self.unit: str = unit
class Value(NamedTuple):
"""Represents a weather value"""
value: Optional[float]
unit: str

def __str__(self):
return f"{self.value} {self.unit}"


class WeatherData:
def __init__(self, timestamp: datetime, values: Dict[str, float]):

def to_value(o: Dict[str, float], variable_name: str, unit: str) -> Value:
value = o.get(variable_name, None)
return Value(value, unit)

self.time: datetime = timestamp

self.temperature: Value = to_value(values, 'Temperature', '°C')
self.dew_point: Value = to_value(values, 'DewPoint', '°C')

self.pressure: Value = to_value(values, 'Pressure', 'hPa')
self.humidity: Value = to_value(values, 'Humidity', '%')

self.wind_direction: Value = to_value(values, 'WindDirection', '°')
self.wind_speed: Value = to_value(values, 'WindSpeedMS', 'm/s')
self.wind_u_component: Value = to_value(values, 'WindUMS', 'm/s')
self.wind_v_component: Value = to_value(values, 'WindVMS', 'm/s')
self.wind_max: Value = to_value(values, 'MaximumWind', 'm/s') # Max 10 min average
self.wind_gust: Value = to_value(values, 'WindGust', 'm/s') # Max 3 sec average

self.symbol: Value = to_value(values, 'WeatherSymbol3', '')
self.cloud_cover: Value = to_value(values, 'TotalCloudCover', '%')

self.cloud_low_cover: Value = to_value(values, 'LowCloudCover', '%')
self.cloud_mid_cover: Value = to_value(values, 'MediumCloudCover', '%')
self.cloud_high_cover: Value = to_value(values, 'HighCloudCover', '%')

# Amount of rain in the past 1h
self.precipitation_amount: Value = to_value(values, 'Precipitation1h', 'mm/h')

# No idea what this is since it is missing the time
# self.precipitation_amount: Value = to_value(values, 'PrecipitationAmount', 'mm')

# Short wave radiation (light, UV) accumulation
self.radiation_short_wave_acc: Value = to_value(values, 'RadiationGlobalAccumulation', 'J/m²')

# Short wave radiation (light, UV) net accumulation on the surface
self.radiation_short_wave_surface_net_acc: Value = to_value(values, 'RadiationNetSurfaceSWAccumulation', 'J/m²')

# Long wave radiation (heat, infrared) accumulation
self.radiation_long_wave_acc: Value = to_value(values, 'RadiationLWAccumulation', 'J/m²')

# Long wave radiation (light, UV) net accumulation on the surface
self.radiation_long_wave_surface_net_acc: Value = to_value(values, 'RadiationNetSurfaceLWAccumulation', 'J/m²')

# Diffused short wave
self.radiation_short_wave_diff_surface_acc: Value = to_value(values, 'RadiationDiffuseAccumulation', 'J/m²')

self.geopotential_height: Value = to_value(values, 'GeopHeight', 'm')
self.land_sea_mask: Value = to_value(values, 'LandSeaMask', '')


class Weather:
def __init__(self, place: str, lat: float, lon: float, weather_data: WeatherData):
self.place: str = place
self.lat: float = lat
self.lon: float = lon
self.data: WeatherData = weather_data


class Forecast:
def __init__(self, place: str, lat: float, lon: float, forecasts: List[WeatherData]):
self.place: str = place
self.lat: float = lat
self.lon: float = lon
self.forecasts: List[WeatherData] = forecasts
class WeatherData(NamedTuple):
"""Represents a weather"""
time: datetime
temperature: Value
dew_point: Value
pressure: Value
humidity: Value
wind_direction: Value
wind_speed: Value
wind_u_component: Value
wind_v_component: Value
wind_max: Value # Max 10 minutes average
wind_gust: Value # Max 3 seconds average
symbol: Value
cloud_cover: Value
cloud_low_cover: Value
cloud_mid_cover: Value
cloud_high_cover: Value

# Amount of rain in the past 1h
precipitation_amount: Value

# Short wave radiation (light, UV) accumulation
radiation_short_wave_acc: Value

# Short wave radiation (light, UV) net accumulation on the surface
radiation_short_wave_surface_net_acc: Value

# Long wave radiation (heat, infrared) accumulation
radiation_long_wave_acc: Value

# Long wave radiation (light, UV) net accumulation on the surface
radiation_long_wave_surface_net_acc: Value

# Diffused short wave
radiation_short_wave_diff_surface_acc: Value

geopotential_height: Value
land_sea_mask: Value


class Weather(NamedTuple):
"""Represents a weather"""
place: str
lat: float
lon: float
data: WeatherData


class Forecast(NamedTuple):
"""Represents a forecast"""
place: str
lat: float
lon: float
forecasts: List[WeatherData]
Loading

0 comments on commit a4712b1

Please sign in to comment.