Skip to content

Commit

Permalink
Add support of free api (#7)
Browse files Browse the repository at this point in the history
* freemium

* add freemium

* freemium

* fix style

* fix
  • Loading branch information
freekode authored Jun 18, 2024
1 parent eb1a6fb commit 9d69c63
Show file tree
Hide file tree
Showing 13 changed files with 411 additions and 111 deletions.
3 changes: 2 additions & 1 deletion src/pyopenweathermap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
UnauthorizedError,
TooManyRequestsError
)
from .owm_client import OWMClient
from .client.owm_abstract_client import OWMClient
from .client.owm_client_factory import OWMClientFactory
from .weather import (
CurrentWeather, HourlyWeatherForecast, DailyWeatherForecast, WeatherReport, DailyTemperature, WeatherCondition
)
44 changes: 44 additions & 0 deletions src/pyopenweathermap/client/freemium_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from .owm_abstract_client import OWMClient
from ..data_converter import DataConverter
from ..exception import UnauthorizedError
from ..weather import WeatherReport

CURRENT_WEATHER_API_URL = 'https://api.openweathermap.org/data/2.5/weather'
FORECAST_API_URL = 'https://api.openweathermap.org/data/2.5/forecast'


class OWMFreemiumClient(OWMClient):
def __init__(self, api_key, api_type, units="metric", lang='en'):
super().__init__()
self.api_key = api_key
self.api_type = api_type
self.units = units
self.lang = lang

async def get_weather(self, lat, lon) -> WeatherReport:
url = self._get_url(lat, lon)
json_response = await self.http_client.request(url)

current, hourly = None, []
if self.api_type == 'current':
current = DataConverter.freemium_to_current_weather(json_response)
else:
hourly = [DataConverter.freemium_to_hourly_weather_forecast(item) for item in json_response['list']]
return WeatherReport(current, hourly, [])

async def validate_key(self) -> bool:
url = self._get_url(50.06, 14.44)
try:
await self.http_client.request(url)
return True
except UnauthorizedError:
return False

def _get_url(self, lat, lon):
url = CURRENT_WEATHER_API_URL if self.api_type == 'current' else FORECAST_API_URL
return (f"{url}?"
f"lat={lat}&"
f"lon={lon}&"
f"appid={self.api_key}&"
f"units={self.units}&"
f"lang={self.lang}")
48 changes: 48 additions & 0 deletions src/pyopenweathermap/client/onecall_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from .owm_abstract_client import OWMClient
from ..data_converter import DataConverter
from ..exception import UnauthorizedError
from ..weather import WeatherReport

V30_API_URL = 'https://api.openweathermap.org/data/3.0/onecall'
V25_API_URL = 'https://api.openweathermap.org/data/2.5/onecall'


class OWMOneCallClient(OWMClient):
def __init__(self, api_key, api_version, units="metric", lang='en'):
super().__init__()
self.api_key = api_key
self.api_version = api_version
self.units = units
self.lang = lang

async def get_weather(self, lat, lon) -> WeatherReport:
url = self._get_url(lat, lon)
json_response = await self.http_client.request(url)

current, hourly, daily = None, [], []
if json_response.get('current') is not None:
current = DataConverter.onecall_to_current_weather(json_response['current'])
if json_response.get('hourly') is not None:
hourly = [DataConverter.onecall_to_hourly_weather_forecast(item) for item in json_response['hourly']]
if json_response.get('daily') is not None:
daily = [DataConverter.onecall_to_daily_weather_forecast(item) for item in json_response['daily']]

return WeatherReport(current, hourly, daily)

async def validate_key(self) -> bool:
url = (f"{self._get_url(50.06, 14.44)}"
f"&exclude=current,minutely,hourly,daily,alerts)")
try:
await self.http_client.request(url)
return True
except UnauthorizedError:
return False

def _get_url(self, lat, lon):
url = V30_API_URL if self.api_version == 'v3.0' else V25_API_URL
return (f"{url}?"
f"lat={lat}&"
f"lon={lon}&"
f"appid={self.api_key}&"
f"units={self.units}&"
f"lang={self.lang}")
18 changes: 18 additions & 0 deletions src/pyopenweathermap/client/owm_abstract_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from abc import ABC, abstractmethod
from ..weather import WeatherReport
from ..http_client import HttpClient


class OWMClient(ABC):
http_client: HttpClient

def __init__(self) -> None:
self.http_client = HttpClient()

@abstractmethod
async def get_weather(self, lat, lon) -> WeatherReport:
pass

@abstractmethod
async def validate_key(self) -> bool:
pass
18 changes: 18 additions & 0 deletions src/pyopenweathermap/client/owm_client_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging

from .freemium_client import OWMFreemiumClient
from .onecall_client import OWMOneCallClient


class OWMClientFactory:
@staticmethod
def get_client(api_key, api_type, units="metric", lang='en'):
logger = logging.getLogger(__name__)

logger.info('Initializing OWMClient with api type: ' + str(api_type))
if api_type == 'v3.0' or api_type == 'v2.5':
return OWMOneCallClient(api_key, api_type, units, lang)
if api_type == 'current' or api_type == 'forecast':
return OWMFreemiumClient(api_key, api_type, units, lang)
else:
raise Exception('Unsupported API type ' + str(api_type))
47 changes: 44 additions & 3 deletions src/pyopenweathermap/data_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

class DataConverter:
@staticmethod
def to_current_weather(json):
def onecall_to_current_weather(json):
return CurrentWeather(
date_time=datetime.fromtimestamp(json['dt'], tz=UTC),
temperature=json['temp'],
Expand All @@ -25,7 +25,7 @@ def to_current_weather(json):
)

@staticmethod
def to_hourly_weather_forecast(json):
def onecall_to_hourly_weather_forecast(json):
return HourlyWeatherForecast(
date_time=datetime.fromtimestamp(json['dt'], tz=UTC),
temperature=json['temp'],
Expand All @@ -46,7 +46,7 @@ def to_hourly_weather_forecast(json):
)

@staticmethod
def to_daily_weather_forecast(json):
def onecall_to_daily_weather_forecast(json):
return DailyWeatherForecast(
date_time=datetime.fromtimestamp(json['dt'], tz=UTC),
summary=json.get('summary'),
Expand All @@ -65,6 +65,47 @@ def to_daily_weather_forecast(json):
snow=json.get('snow', 0),
condition=DataConverter._to_weather_condition(json['weather'][0]),
)

@staticmethod
def freemium_to_current_weather(json):
return CurrentWeather(
date_time=datetime.fromtimestamp(json['dt'], tz=UTC),
temperature=json['main']['temp'],
feels_like=json['main']['feels_like'],
pressure=json['main']['pressure'],
humidity=json['main']['humidity'],
dew_point=None,
uv_index=None,
cloud_coverage=json['clouds'],
visibility=json['visibility'],
wind_speed=json['wind']['speed'],
wind_gust=json['wind'].get('gust'),
wind_bearing=json['wind']['deg'],
rain=json.get('rain', {}),
snow=json.get('snow', {}),
condition=DataConverter._to_weather_condition(json['weather'][0]),
)

@staticmethod
def freemium_to_hourly_weather_forecast(json):
return HourlyWeatherForecast(
date_time=datetime.fromtimestamp(json['dt'], tz=UTC),
temperature=json['main']['temp'],
feels_like=json['main']['feels_like'],
pressure=json['main']['pressure'],
humidity=json['main']['humidity'],
dew_point=None,
uv_index=None,
cloud_coverage=json['clouds']['all'],
visibility=json.get('visibility', None),
wind_speed=json['wind']['speed'],
wind_gust=json['wind'].get('gust'),
wind_bearing=json['wind']['deg'],
precipitation_probability=json.get('pop', 0),
rain=json.get('rain', {}),
snow=json.get('snow', {}),
condition=DataConverter._to_weather_condition(json['weather'][0]),
)

@staticmethod
def _to_weather_condition(json):
Expand Down
36 changes: 36 additions & 0 deletions src/pyopenweathermap/http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from aiohttp import ClientSession

from .exception import UnauthorizedError, RequestError, TooManyRequestsError
import logging

class HttpClient:
request_timeout: int
logger = logging.getLogger(__name__)

def __init__(self, request_timeout=20):
self.request_timeout = request_timeout

async def request(self, url):
self.logger.debug('Requesting url: ' + url)
async with ClientSession() as session:
try:
async with session.get(url=url, timeout=self.request_timeout) as response:
response_json = await response.json()
if response.status == 200:
return response_json
elif response.status == 400:
raise RequestError(response_json.get('message'))
elif response.status == 401:
raise UnauthorizedError(response_json.get('message'))
elif response.status == 404:
raise RequestError(response_json.get('message'))
elif response.status == 429:
raise TooManyRequestsError(response_json.get('message'))
else:
raise RequestError("Unknown status code: {}".format(response.status))
except RequestError as error:
raise error
except TimeoutError:
raise RequestError("Request timeout")
except Exception as error:
raise RequestError(error) from error
91 changes: 0 additions & 91 deletions src/pyopenweathermap/owm_client.py

This file was deleted.

49 changes: 49 additions & 0 deletions tests/freemium_current.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"coord": {
"lon": 10.99,
"lat": 44.34
},
"weather": [
{
"id": 501,
"main": "Rain",
"description": "moderate rain",
"icon": "10d"
}
],
"base": "stations",
"main": {
"temp": 298.48,
"feels_like": 298.74,
"temp_min": 297.56,
"temp_max": 300.05,
"pressure": 1015,
"humidity": 64,
"sea_level": 1015,
"grnd_level": 933
},
"visibility": 10000,
"wind": {
"speed": 0.62,
"deg": 349,
"gust": 1.18
},
"rain": {
"1h": 3.16
},
"clouds": {
"all": 100
},
"dt": 1661870592,
"sys": {
"type": 2,
"id": 2075663,
"country": "IT",
"sunrise": 1661834187,
"sunset": 1661882248
},
"timezone": 7200,
"id": 3163858,
"name": "Zocca",
"cod": 200
}
Loading

0 comments on commit 9d69c63

Please sign in to comment.