diff --git a/Pipfile b/Pipfile index 5b6cf1e..dd24a3d 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ flask-restful = "~=0.3" flask-caching = "~=1.10" requests = "~=2.25" schematics = "~=2.1" +unidecode = "~=1.2" [dev-packages] pytest = "~=6.2" diff --git a/Pipfile.lock b/Pipfile.lock index 4121132..b065ace 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0bec2450146784c9961652c4a44995a4a78b6bdfcbec60d9a87e628340c317b1" + "sha256": "d2505661c629b8bc51a9e6dae2e7939cac45143b1c445e77e81f1aa122b3502c" }, "pipfile-spec": 6, "requires": { @@ -176,6 +176,14 @@ ], "version": "==1.15.0" }, + "unidecode": { + "hashes": [ + "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00", + "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d" + ], + "index": "pypi", + "version": "==1.2.0" + }, "urllib3": { "hashes": [ "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", diff --git a/api/models/weather.py b/api/models/weather.py index 917eea6..7ff8de7 100644 --- a/api/models/weather.py +++ b/api/models/weather.py @@ -1,11 +1,92 @@ from schematics.models import Model from schematics.types import FloatType, GeoPointType, ModelType, StringType +from schematics.types.serializable import serializable +from unidecode import unidecode +from api.utils.metatypes import EnumMeta class PhysicalQuantity(Model): value = FloatType(required=True) unit = StringType(required=True) +class Temperature(Model): + value = FloatType(required=True) + + class TemperatureMetrics(StringType, metaclass=EnumMeta): + CELSIUS = 'CELSIUS' + FAHRENHEIT = 'FAHRENHEIT' + KELVIN = 'KELVIN' + + @serializable(type=FloatType, serialized_name='value') + def get_temperature(self, *args, **kwargs): + if hasattr(self, 'context'): + temperature = self.context.get('temperature', 'kelvin') + + if temperature.upper() == self.TemperatureMetrics.CELSIUS: + value = round(self.value - 273.0, 2) # Converts Kelvin temperature to Celsius + elif temperature.upper() == self.TemperatureMetrics.FAHRENHEIT: + value = round(1.8*(self.value-273.0)+32.0, 2) # Converts Kelvin temperature to Fahrenheit + else: + value = round(self.value, 2) # Returns Kelvin temperature + else: + value = round(self.value, 2) # Returns Kelvin temperature + return value + + @serializable(type=StringType, serialized_name='unit') + def get_unit(self, *args, **kwargs): + if hasattr(self, 'context'): + unit = self.context.get('temperature', 'kelvin') + + if unit.upper() == self.TemperatureMetrics.CELSIUS: + unit = self.TemperatureMetrics.CELSIUS + elif unit.upper() == self.TemperatureMetrics.FAHRENHEIT: + unit = self.TemperatureMetrics.FAHRENHEIT + else: + unit = self.TemperatureMetrics.KELVIN + else: + unit = self.TemperatureMetrics.KELVIN + return unit.lower() + + +class Distance(Model): + value = FloatType(required=True) + + class DistanceMetrics(StringType, metaclass=EnumMeta): + METERS = 'METERS' + KILOMETERS = 'KILOMETERS' + MILES = 'MILES' + + @serializable(type=FloatType, serialized_name='value') + def get_distance(self, *args, **kwargs): + if hasattr(self, 'context'): + distance = self.context.get('distance', 'meters') + + if distance.upper() == self.DistanceMetrics.KILOMETERS: + value = round(self.value / 1000, 2) # Converts Meters distance to Kilometers + elif distance.upper() == self.DistanceMetrics.MILES: + value = round(self.value / 1609.344, 2) # Converts Meters distance to Miles + else: + value = round(self.value, 2) # Returns Meters distance + else: + value = round(self.value, 2) # Returns Meters distance + return value + + @serializable(type=StringType, serialized_name='unit') + def get_unit(self, *args, **kwargs): + if hasattr(self, 'context'): + unit = self.context.get('distance', 'meters') + + if unit.upper() == self.DistanceMetrics.KILOMETERS: + unit = self.DistanceMetrics.KILOMETERS + elif unit.upper() == self.DistanceMetrics.MILES: + unit = self.DistanceMetrics.MILES + else: + unit = self.DistanceMetrics.METERS + else: + unit = self.DistanceMetrics.METERS + return unit.lower() + + class City(Model): name = StringType(required=True) country = StringType() @@ -27,12 +108,16 @@ class Weather(Model): city = ModelType(City, required=True) description = StringType(required=True) long_description = StringType(required=True) - temperature = ModelType(PhysicalQuantity, required=True) - feels_like = ModelType(PhysicalQuantity) - max_temperature = ModelType(PhysicalQuantity) - min_temperature = ModelType(PhysicalQuantity) + temperature = ModelType(Temperature, required=True) + feels_like = ModelType(Temperature) + max_temperature = ModelType(Temperature) + min_temperature = ModelType(Temperature) wind = ModelType(Wind, required=True) - visibility = ModelType(PhysicalQuantity) + visibility = ModelType(Distance) class Options: serialize_when_none = False + + @serializable(type=StringType, serialized_name='id') + def id(self): + return unidecode(f'{self.city.name.upper()}') diff --git a/api/resources/weather.py b/api/resources/weather.py index 031c5a5..6c7281c 100644 --- a/api/resources/weather.py +++ b/api/resources/weather.py @@ -1,5 +1,6 @@ from flask import request from flask_restful import Resource, reqparse +from unidecode import unidecode from api import cache from api import openweathermap as owm from api.models.weather import Weather @@ -30,20 +31,52 @@ def get(self): return recent_weather_list, 200 class WeatherView(Resource): - @cache.cached() def get(self, city_name): - # Request the weather in the specify city - response = owm.current_weather(city_name) + # Recovers the cached weather list + weather_list = cache.get('weather_list') + weather_cached = None - # Recover and update the weather list in the cache + if weather_list is not None: + for weather in weather_list: + # Gets the weather in cache if exists + if unidecode(city_name.upper()) == weather['id']: + weather_cached = weather + else: + weather_list = [] + + if weather_cached is not None: + # Instances an existing weather in cache + response = Weather(weather_cached) + else: + # Requests a new weather with the OpenWeatherMap API + response = owm.current_weather(city_name) + + if isinstance(response, Weather): + weather_list.append(response.to_primitive()) + cache.set('weather_list', weather_list) + + # Creates the context of the units of measurement if isinstance(response, Weather): weather = response - weather_list = cache.get('weather_list') if cache.get('weather_list') is not None else [] - weather_list.append(weather.to_primitive()) - cache.set('weather_list', weather_list) + + # Creates the temperature context + temperature = request.args.get('temperature', 'kelvin') + temperature_context = {'temperature': temperature} + weather.temperature.context = temperature_context + if weather.feels_like is not None: + weather.feels_like.context = temperature_context + if weather.max_temperature is not None: + weather.max_temperature.context = temperature_context + if weather.min_temperature is not None: + weather.min_temperature.context = temperature_context + + # Creates the distance context + distance = request.args.get('distance', 'meters') + distance_context = {'distance': distance} + weather.visibility.context = distance_context else: return response - return weather.serialize(), 200 + return weather.to_primitive(), 200 diff --git a/api/utils/metatypes.py b/api/utils/metatypes.py new file mode 100644 index 0000000..51a4609 --- /dev/null +++ b/api/utils/metatypes.py @@ -0,0 +1,8 @@ +from schematics.types.base import TypeMeta + +# Inheriting this class will make an enum exhaustive +class EnumMeta(TypeMeta): + def __new__(mcs, name, bases, attrs): + attrs['choices'] = [v for k, v in attrs.items( + ) if not k.startswith('_') and k.isupper()] + return TypeMeta.__new__(mcs, name, bases, attrs) diff --git a/api/utils/openweathermap.py b/api/utils/openweathermap.py index fdee835..7595239 100644 --- a/api/utils/openweathermap.py +++ b/api/utils/openweathermap.py @@ -1,5 +1,5 @@ import os, requests -from api.models.weather import PhysicalQuantity, City, Wind, Weather +from api.models.weather import PhysicalQuantity, Temperature, Distance, City, Wind, Weather class OpenWeatherMap: @@ -23,21 +23,17 @@ def format_weather(response): }), 'description': response['weather'][0]['main'].title(), 'long_description': response['weather'][0]['description'].title(), - 'temperature': PhysicalQuantity({ - 'value': float(response['main']['temp']), - 'unit': 'celsius degree' + 'temperature': Temperature({ + 'value': float(response['main']['temp']) }), - 'feels_like': PhysicalQuantity({ - 'value': float(response['main']['feels_like']), - 'unit': 'celsius degree' + 'feels_like': Temperature({ + 'value': float(response['main']['feels_like']) }) if response['main']['feels_like'] != response['main']['temp'] else None, - 'max_temperature': PhysicalQuantity({ - 'value': float(response['main']['temp_max']), - 'unit': 'celsius degree' + 'max_temperature': Temperature({ + 'value': float(response['main']['temp_max']) }) if response['main']['temp_max'] != response['main']['temp'] else None, - 'min_temperature': PhysicalQuantity({ - 'value': float(response['main']['temp_min']), - 'unit': 'celsius degree' + 'min_temperature': Temperature({ + 'value': float(response['main']['temp_min']) }) if response['main']['temp_min'] != response['main']['temp'] else None, 'wind': Wind({ 'speed': PhysicalQuantity({ @@ -49,9 +45,8 @@ def format_weather(response): 'unit': 'degrees' }) }), - 'visibility': PhysicalQuantity({ - 'value': float(response['visibility']), - 'unit': 'meters' + 'visibility': Distance({ + 'value': float(response['visibility']) }) })