diff --git a/README.md b/README.md index 86716352..adafe0ce 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,16 @@ All requests must be made to the base url: ``https://coronavirus-tracker-api.herokuapp.com/v2/`` (e.g: https://coronavirus-tracker-api.herokuapp.com/v2/locations). You can try them out in your browser to further inspect responses. +### Picking data source + +We provide multiple data-sources you can pick from, simply add the query paramater ``?source=your_source_of_choice`` to your requests. JHU will be used as a default if you don't provide one. + +#### Available sources: + +* **jhu** - https://github.com/CSSEGISandData/COVID-19 - Data repository operated by the Johns Hopkins University Center for Systems Science and Engineering (JHU CSSE). + +* **... more to come later**. + ### Getting latest amount of total confirmed cases, deaths, and recoveries. ```http GET /v2/latest diff --git a/app/__init__.py b/app/__init__.py index 70c1deb7..9861d8b9 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,7 +8,6 @@ def create_app(): """ Construct the core application. """ - # Create flask app with CORS enabled. app = Flask(__name__) CORS(app) diff --git a/app/data/__init__.py b/app/data/__init__.py index e69de29b..518737b3 100644 --- a/app/data/__init__.py +++ b/app/data/__init__.py @@ -0,0 +1,15 @@ +from ..services.location.jhu import JhuLocationService + +# Mapping of services to data-sources. +data_sources = { + 'jhu': JhuLocationService(), +} + +def data_source(source): + """ + Retrieves the provided data-source service. + + :returns: The service. + :rtype: LocationService + """ + return data_sources.get(source.lower()) \ No newline at end of file diff --git a/app/location.py b/app/location/__init__.py similarity index 55% rename from app/location.py rename to app/location/__init__.py index 9d552371..c60f362a 100644 --- a/app/location.py +++ b/app/location/__init__.py @@ -1,5 +1,5 @@ -from .coordinates import Coordinates -from .utils import countrycodes +from ..coordinates import Coordinates +from ..utils import countrycodes class Location: """ @@ -13,12 +13,11 @@ def __init__(self, id, country, province, coordinates, confirmed, deaths, recove self.province = province.strip() self.coordinates = coordinates - # Data. + # Statistics. self.confirmed = confirmed self.deaths = deaths self.recovered = recovered - - + @property def country_code(self): """ @@ -26,38 +25,65 @@ def country_code(self): """ return (countrycodes.country_code(self.country) or countrycodes.default_code).upper() - def serialize(self, timelines = False): + def serialize(self): """ Serializes the location into a dict. - :param timelines: Whether to include the timelines. :returns: The serialized location. :rtype: dict """ - serialized = { + return { # General info. 'id' : self.id, 'country' : self.country, - 'province' : self.province, 'country_code': self.country_code, + 'province' : self.province, # Coordinates. 'coordinates': self.coordinates.serialize(), - # Latest data. + # Latest data (statistics). 'latest': { - 'confirmed': self.confirmed.latest, - 'deaths' : self.deaths.latest, - 'recovered': self.recovered.latest + 'confirmed': self.confirmed, + 'deaths' : self.deaths, + 'recovered': self.recovered }, } +class TimelinedLocation(Location): + """ + A location with timelines. + """ + + def __init__(self, id, country, province, coordinates, timelines): + super().__init__( + # General info. + id, country, province, coordinates, + + # Statistics (retrieve latest from timelines). + confirmed=timelines.get('confirmed').latest, + deaths=timelines.get('deaths').latest, + recovered=timelines.get('recovered').latest, + ) + + # Set timelines. + self.timelines = timelines + + def serialize(self, timelines = False): + """ + Serializes the location into a dict. + + :param timelines: Whether to include the timelines. + :returns: The serialized location. + :rtype: dict + """ + serialized = super().serialize() + # Whether to include the timelines or not. if timelines: serialized.update({ 'timelines': { - 'confirmed': self.confirmed.serialize(), - 'deaths' : self.deaths.serialize(), - 'recovered': self.recovered.serialize(), + # Serialize all the timelines. + key: value.serialize() for (key, value) in self.timelines.items() }}) # Return the serialized location. diff --git a/app/routes/__init__.py b/app/routes/__init__.py index e57a75ea..b890a031 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,4 +1,5 @@ -from flask import Blueprint, redirect, current_app as app +from flask import Blueprint, redirect, request, current_app as app +from ..data import data_source # Follow the import order to avoid circular dependency api_v1 = Blueprint('api_v1', __name__, url_prefix='') @@ -14,3 +15,16 @@ @app.route('/') def index(): return redirect('https://github.com/ExpDev07/coronavirus-tracker-api', 302) + +# Middleware for picking data source. +@api_v2.before_request +def datasource(): + """ + Attaches the datasource to the request. + """ + # Retrieve the datas ource from query param. + source = request.args.get('source', type=str, default='jhu') + + # Attach source to request and return it. + request.source = data_source(source) + pass diff --git a/app/routes/v2/__init__.py b/app/routes/v2/__init__.py index 8b137891..e69de29b 100644 --- a/app/routes/v2/__init__.py +++ b/app/routes/v2/__init__.py @@ -1 +0,0 @@ - diff --git a/app/routes/v2/latest.py b/app/routes/v2/latest.py index f30ebc63..431bb8cd 100644 --- a/app/routes/v2/latest.py +++ b/app/routes/v2/latest.py @@ -1,19 +1,18 @@ -from flask import jsonify +from flask import request, jsonify from ...routes import api_v2 as api -from ...services import jhu @api.route('/latest') def latest(): # Get the serialized version of all the locations. - locations = [ location.serialize() for location in jhu.get_all() ] + locations = request.source.get_all() # All the latest information. - latest = list(map(lambda location: location['latest'], locations)) + # 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)), + 'confirmed': sum(map(lambda location: location.confirmed, locations)), + 'deaths' : sum(map(lambda location: location.deaths, locations)), + 'recovered': sum(map(lambda location: location.recovered, locations)), } }) diff --git a/app/routes/v2/locations.py b/app/routes/v2/locations.py index 4d9937b6..8d443924 100644 --- a/app/routes/v2/locations.py +++ b/app/routes/v2/locations.py @@ -1,7 +1,6 @@ from flask import jsonify, request from distutils.util import strtobool from ...routes import api_v2 as api -from ...services import jhu @api.route('/locations') def locations(): @@ -10,7 +9,7 @@ def locations(): country_code = request.args.get('country_code', type=str) # Retrieve all the locations. - locations = jhu.get_all() + locations = request.source.get_all() # Filtering my country code if provided. if not country_code is None: @@ -30,5 +29,5 @@ def location(id): # Return serialized location. return jsonify({ - 'location': jhu.get(id).serialize(timelines) + 'location': request.source.get(id).serialize(timelines) }) diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index 621c56ca..08f3bb1b 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -1,5 +1,5 @@ from . import LocationService -from ...location import Location +from ...location import TimelinedLocation from ...coordinates import Coordinates from ...timeline import Timeline @@ -16,6 +16,8 @@ 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 @@ -121,15 +123,17 @@ def get_locations(): # Grab coordinates. coordinates = location['coordinates'] - # Create location and append. - locations.append(Location( + # Create location (supporting timelines) and append. + locations.append(TimelinedLocation( # General info. index, location['country'], location['province'], Coordinates(coordinates['lat'], coordinates['long']), # Timelines (parse dates as ISO). - Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }), - Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }), - Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() }) + { + 'confirmed': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }), + 'deaths' : Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }), + 'recovered': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() }) + } )) # Finally, return the locations. diff --git a/tests/test_location.py b/tests/test_location.py index 43840e32..d53f55aa 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -6,7 +6,7 @@ def mocked_timeline(*args, **kwargs): class TestTimeline: def __init__(self, latest): self.latest = latest - + return TestTimeline(args[0]) @pytest.mark.parametrize("test_id, country, country_code, province, latitude, longitude, \ @@ -19,25 +19,37 @@ def test_location_class(mocked_timeline, test_id, country, country_code, provinc longitude, confirmed_latest, deaths_latest, recovered_latest): # id, country, province, coordinates, confirmed, deaths, recovered - coordinate = coordinates.Coordinates(latitude=latitude, longitude=longitude) + coords = coordinates.Coordinates(latitude=latitude, longitude=longitude) + + # Timelines confirmed = timeline.Timeline(confirmed_latest) deaths = timeline.Timeline(deaths_latest) recovered = timeline.Timeline(recovered_latest) - location_obj = location.Location(test_id, country, province, coordinate, - confirmed, deaths, recovered) + # Location. + location_obj = location.TimelinedLocation(test_id, country, province, coords, { + 'confirmed': confirmed, + 'deaths' : deaths, + 'recovered': recovered, + }) assert location_obj.country_code == country_code #validate serialize - check_dict = {'id': test_id, - 'country': country, - 'province': province, - 'country_code': country_code, - 'coordinates': {'latitude': latitude, - 'longitude': longitude}, - 'latest': {'confirmed': confirmed_latest, - 'deaths': deaths_latest, - 'recovered': recovered_latest}} + check_dict = { + 'id': test_id, + 'country': country, + 'country_code': country_code, + 'province': province, + 'coordinates': { + 'latitude': latitude, + 'longitude': longitude + }, + 'latest': { + 'confirmed': confirmed_latest, + 'deaths': deaths_latest, + 'recovered': recovered_latest + } + } assert location_obj.serialize() == check_dict