Skip to content
Merged
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
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