Skip to content
This repository was archived by the owner on Aug 17, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 90 additions & 5 deletions api/models/weather.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this is quite an improvement from what we've had so far. Congrats.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of rounding to 2 dpp every time you could use a custom type inheriting from FloatType which specifies that centrally.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a side note, rounding floats is not very reliable. This reference is quite outdated but still relevant.

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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Distance and Temperature could inherit from an generic abstract "Number with unit" data type, leaving only the metrics, the default one and the transformation functions between each pair (or to and from default) to be defined in each of these.

That would add quite a bit of complexity though and would only be justifiable for production code if there were to exist more classes like these.

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()
Expand All @@ -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()}')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At PassFort's team we use UUID. Please, take a look.

49 changes: 41 additions & 8 deletions api/resources/weather.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
8 changes: 8 additions & 0 deletions api/utils/metatypes.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I've sent this to you, but do you understand what it does?

27 changes: 11 additions & 16 deletions api/utils/openweathermap.py
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one should follow the changes of the other fields as well.

'speed': PhysicalQuantity({
Expand All @@ -49,9 +45,8 @@ def format_weather(response):
'unit': 'degrees'
})
}),
'visibility': PhysicalQuantity({
'value': float(response['visibility']),
'unit': 'meters'
'visibility': Distance({
'value': float(response['visibility'])
})
})

Expand Down