Skip to content

Commit

Permalink
Merge pull request #115 from ExpDev07/multiple-sources
Browse files Browse the repository at this point in the history
Basis for providing multiple data-sources.
  • Loading branch information
ExpDev07 authored Mar 21, 2020
2 parents a41f38d + dbc22e7 commit 7208fd0
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 48 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ def create_app():
"""
Construct the core application.
"""

# Create flask app with CORS enabled.
app = Flask(__name__)
CORS(app)
Expand Down
15 changes: 15 additions & 0 deletions app/data/__init__.py
Original file line number Diff line number Diff line change
@@ -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())
58 changes: 42 additions & 16 deletions app/location.py → app/location/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .coordinates import Coordinates
from .utils import countrycodes
from ..coordinates import Coordinates
from ..utils import countrycodes

class Location:
"""
Expand All @@ -13,51 +13,77 @@ 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):
"""
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, 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.
Expand Down
16 changes: 15 additions & 1 deletion app/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -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='')
Expand All @@ -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
1 change: 0 additions & 1 deletion app/routes/v2/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@

13 changes: 6 additions & 7 deletions app/routes/v2/latest.py
Original file line number Diff line number Diff line change
@@ -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)),
}
})
5 changes: 2 additions & 3 deletions app/routes/v2/locations.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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:
Expand All @@ -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)
})
16 changes: 10 additions & 6 deletions app/services/location/jhu.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from . import LocationService
from ...location import Location
from ...location import TimelinedLocation
from ...coordinates import Coordinates
from ...timeline import Timeline

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
38 changes: 25 additions & 13 deletions tests/test_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand All @@ -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

0 comments on commit 7208fd0

Please sign in to comment.