diff --git a/app/__init__.py b/app/__init__.py index dc2a691e..6414cb3e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,5 @@ from flask import Flask from flask_cors import CORS -from . import settings def create_app(): """ @@ -12,7 +11,7 @@ def create_app(): CORS(app) # Set app config from settings. - app.config.from_pyfile('settings.py'); + app.config.from_pyfile('config/settings.py'); with app.app_context(): # Import routes. diff --git a/app/settings.py b/app/config/settings.py similarity index 100% rename from app/settings.py rename to app/config/settings.py diff --git a/app/coordinates.py b/app/coordinates.py new file mode 100644 index 00000000..0243e637 --- /dev/null +++ b/app/coordinates.py @@ -0,0 +1,20 @@ +class Coordinates: + """ + A position on earth using decimal coordinates (latitude and longitude). + """ + + def __init__(self, latitude, longitude): + self.latitude = latitude + self.longitude = longitude + + def serialize(self): + """ + Serializes the coordinates into a dict. + """ + return { + 'latitude' : self.latitude, + 'longitude': self.longitude + } + + def __str__(self): + return 'lat: %s, long: %s' % (self.latitude, self.longitude) \ No newline at end of file diff --git a/app/location.py b/app/location.py new file mode 100644 index 00000000..e348c697 --- /dev/null +++ b/app/location.py @@ -0,0 +1,49 @@ +from .coordinates import Coordinates +from .utils import countrycodes + +class Location: + """ + A location in the world affected by the coronavirus. + """ + + def __init__(self, id, country, province, coordinates, confirmed, deaths, recovered): + # General info. + self.id = id + self.country = country.strip() + self.province = province.strip() + self.coordinates = coordinates + + # Data. + self.confirmed = confirmed + self.deaths = deaths + self.recovered = recovered + + + @property + def country_code(self): + """ + Gets the alpha-2 code represention of the country. Returns 'XX' if none is found. + """ + return (countrycodes.country_code(self.country) or countrycodes.default_code).upper() + + def serialize(self): + """ + Serializes the location into a dict. + """ + return { + # General info. + 'id' : self.id, + 'country' : self.country, + 'province' : self.province, + 'country_code': self.country_code, + + # Coordinates. + 'coordinates': self.coordinates.serialize(), + + # Latest data. + 'latest': { + 'confirmed': self.confirmed.latest, + 'deaths' : self.deaths.latest, + 'recovered': self.recovered.latest + } + } \ No newline at end of file diff --git a/app/models/location.py b/app/models/location.py deleted file mode 100644 index 5f202b3c..00000000 --- a/app/models/location.py +++ /dev/null @@ -1,4 +0,0 @@ -class Location(): - """ - A location in the world affected by the coronavirus. - """ \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 0e2e3c2c..b2bc5b73 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,4 +1,5 @@ -from . import confirmed -from . import deaths -from . import recovered -from . import all \ No newline at end of file +# API version 1. +from .v1 import confirmed, deaths, recovered, all + +# API version 2. +from .v2 import locations, latest \ No newline at end of file diff --git a/app/routes/all.py b/app/routes/v1/all.py similarity index 72% rename from app/routes/all.py rename to app/routes/v1/all.py index f80e289f..735b4d10 100644 --- a/app/routes/all.py +++ b/app/routes/v1/all.py @@ -1,13 +1,13 @@ from flask import jsonify from flask import current_app as app -from ..data import get_data +from ...services.location.jhu import get_category @app.route('/all') def all(): # Get all the categories. - confirmed = get_data('confirmed') - deaths = get_data('deaths') - recovered = get_data('recovered') + confirmed = get_category('confirmed') + deaths = get_category('deaths') + recovered = get_category('recovered') return jsonify({ # Data. diff --git a/app/routes/confirmed.py b/app/routes/v1/confirmed.py similarity index 52% rename from app/routes/confirmed.py rename to app/routes/v1/confirmed.py index dfa3346d..52fcf7e0 100644 --- a/app/routes/confirmed.py +++ b/app/routes/v1/confirmed.py @@ -1,7 +1,7 @@ from flask import jsonify from flask import current_app as app -from ..data import get_data +from ...services.location.jhu import get_category @app.route('/confirmed') def confirmed(): - return jsonify(get_data('confirmed')) \ No newline at end of file + return jsonify(get_category('confirmed')) \ No newline at end of file diff --git a/app/routes/deaths.py b/app/routes/v1/deaths.py similarity index 52% rename from app/routes/deaths.py rename to app/routes/v1/deaths.py index 61ef7546..76913b6d 100644 --- a/app/routes/deaths.py +++ b/app/routes/v1/deaths.py @@ -1,7 +1,7 @@ from flask import jsonify from flask import current_app as app -from ..data import get_data +from ...services.location.jhu import get_category @app.route('/deaths') def deaths(): - return jsonify(get_data('deaths')) \ No newline at end of file + return jsonify(get_category('deaths')) \ No newline at end of file diff --git a/app/routes/recovered.py b/app/routes/v1/recovered.py similarity index 52% rename from app/routes/recovered.py rename to app/routes/v1/recovered.py index 1abdef26..ded8eb7c 100644 --- a/app/routes/recovered.py +++ b/app/routes/v1/recovered.py @@ -1,7 +1,7 @@ from flask import jsonify from flask import current_app as app -from ..data import get_data +from ...services.location.jhu import get_category @app.route('/recovered') def recovered(): - return jsonify(get_data('recovered')) \ No newline at end of file + return jsonify(get_category('recovered')) \ No newline at end of file diff --git a/app/routes/v2/latest.py b/app/routes/v2/latest.py new file mode 100644 index 00000000..0f3ba265 --- /dev/null +++ b/app/routes/v2/latest.py @@ -0,0 +1,18 @@ +from flask import jsonify, current_app as app +from ...services import jhu + +@app.route('/v2/latest') +def latest(): + # Get the serialized version of all the locations. + locations = [ location.serialize() for location in jhu.get_all() ] + + # All the latest information. + latest = list(map(lambda location: location['latest'], locations)) + + return jsonify({ + 'latest': { + 'confirmed': sum(map(lambda latest: latest['confirmed'], latest)), + 'deaths' : sum(map(lambda latest: latest['deaths'], latest)), + 'recovered': sum(map(lambda latest: latest['recovered'], latest)), + } + }) \ No newline at end of file diff --git a/app/routes/v2/locations.py b/app/routes/v2/locations.py new file mode 100644 index 00000000..eb50e251 --- /dev/null +++ b/app/routes/v2/locations.py @@ -0,0 +1,40 @@ +from flask import jsonify, request, current_app as app +from ...services import jhu + +@app.route('/v2/locations') +def locations(): + # Query parameters. + country_code = request.args.get('country_code', type=str) + + # Retrieve all the locations. + locations = jhu.get_all() + + # Filtering my country code if provided. + if not country_code is None: + locations = list(filter(lambda location: location.country_code == country_code.upper(), locations)) + + # Serialize each location and return. + return jsonify({ + 'locations': [ + location.serialize() for location in locations + ] + }) + +@app.route('/v2/locations/') +def location(id): + # Retrieve location with the provided id. + location = jhu.get(id) + + # Get all the timelines. + timelines = { + 'confirmed': location.confirmed.serialize(), + 'deaths' : location.deaths.serialize(), + 'recovered': location.recovered.serialize(), + } + + # Serialize the location, add timelines, and then return. + return jsonify({ + 'location': { + **jhu.get(id).serialize(), **{ 'timelines': timelines } + } + }) \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 00000000..0c5b206c --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1,4 @@ +from .location.jhu import JhuLocationService + +# Instances of the services. +jhu = JhuLocationService() \ No newline at end of file diff --git a/app/services/location/__init__.py b/app/services/location/__init__.py new file mode 100644 index 00000000..2312283a --- /dev/null +++ b/app/services/location/__init__.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod + +class LocationService(ABC): + """ + Service for retrieving locations. + """ + + @abstractmethod + def get_all(self): + """ + Gets and returns all of the locations. + + :returns: The locations. + :rtype: Location + """ + raise NotImplementedError + + @abstractmethod + def get(self, id): + """ + Gets and returns location with the provided id. + + :returns: The location. + :rtype: Location + """ + raise NotImplementedError \ No newline at end of file diff --git a/app/data/__init__.py b/app/services/location/jhu.py similarity index 50% rename from app/data/__init__.py rename to app/services/location/jhu.py index c3d5ffa1..1c5a9d39 100644 --- a/app/data/__init__.py +++ b/app/services/location/jhu.py @@ -1,18 +1,63 @@ +from . import LocationService +from ...location import Location +from ...coordinates import Coordinates +from ...timeline import Timeline + +class JhuLocationService(LocationService): + """ + Service for retrieving locations from Johns Hopkins CSSE (https://github.com/CSSEGISandData/COVID-19). + """ + + def get_all(self): + # Get all of the data categories locations. + confirmed = get_category('confirmed')['locations'] + deaths = get_category('deaths')['locations'] + recovered = get_category('recovered')['locations'] + + # Final locations to return. + locations = [] + + # Go through confirmed locations. + for index, location in enumerate(confirmed): + # Grab coordinates. + coordinates = location['coordinates'] + + # Create location and append. + locations.append(Location( + # General info. + index, location['country'], location['province'], Coordinates(coordinates['lat'], coordinates['long']), + + # TODO: date key as ISO format. + # { datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': int(amount or 0) for date, amount in history.items() } + + # Timelines. + Timeline(confirmed[index]['history']), + Timeline(deaths[index]['history']), + Timeline(recovered[index]['history']) + )) + + # Finally, return the locations. + return locations + + def get(self, id): + # Get location at the index equal to provided id. + return self.get_all()[id] + import requests import csv from datetime import datetime from cachetools import cached, TTLCache -from ..utils import countrycodes, date as date_util +from ...utils import countrycodes, date as date_util """ -Base URL for fetching data. +Base URL for fetching category. """ base_url = 'https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/time_series_19-covid-%s.csv'; @cached(cache=TTLCache(maxsize=1024, ttl=3600)) -def get_data(category): +def get_category(category): """ - Retrieves the data for the provided type. The data is cached for 1 hour. + Retrieves the data for the provided category. The data is cached for 1 hour. """ # Adhere to category naming standard. @@ -39,7 +84,7 @@ def get_data(category): country = item['Country/Region'] # Latest data insert value. - latest = list(history.values())[-1]; + latest = list(sorted(history.values()))[-1]; # Normalize the item and append to locations. locations.append({ @@ -70,4 +115,4 @@ def get_data(category): 'latest': latest, 'last_updated': datetime.utcnow().isoformat() + 'Z', 'source': 'https://github.com/ExpDev07/coronavirus-tracker-api', - } + } \ No newline at end of file diff --git a/app/timeline.py b/app/timeline.py new file mode 100644 index 00000000..48af1ac5 --- /dev/null +++ b/app/timeline.py @@ -0,0 +1,33 @@ +from datetime import datetime +from collections import OrderedDict + +class Timeline: + """ + Timeline with history of data. + """ + + def __init__(self, history = {}): + self.__timeline = history + + @property + def timeline(self): + """ + Gets the history sorted by date (key). + """ + return OrderedDict(sorted(self.__timeline.items())) + + @property + def latest(self): + """ + Gets the latest available history value. + """ + return list(self.timeline.values())[-1] or 0 + + def serialize(self): + """ + Serializes the data into a dict. + """ + return { + 'latest' : self.latest, + 'timeline': self.timeline + } \ No newline at end of file