Skip to content

Commit 7208fd0

Browse files
authored
Merge pull request ExpDev07#115 from ExpDev07/multiple-sources
Basis for providing multiple data-sources.
2 parents a41f38d + dbc22e7 commit 7208fd0

File tree

10 files changed

+125
-48
lines changed

10 files changed

+125
-48
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515

1616
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.
1717

18+
### Picking data source
19+
20+
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.
21+
22+
#### Available sources:
23+
24+
* **jhu** - https://github.com/CSSEGISandData/COVID-19 - Data repository operated by the Johns Hopkins University Center for Systems Science and Engineering (JHU CSSE).
25+
26+
* **... more to come later**.
27+
1828
### Getting latest amount of total confirmed cases, deaths, and recoveries.
1929
```http
2030
GET /v2/latest

app/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ def create_app():
88
"""
99
Construct the core application.
1010
"""
11-
1211
# Create flask app with CORS enabled.
1312
app = Flask(__name__)
1413
CORS(app)

app/data/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from ..services.location.jhu import JhuLocationService
2+
3+
# Mapping of services to data-sources.
4+
data_sources = {
5+
'jhu': JhuLocationService(),
6+
}
7+
8+
def data_source(source):
9+
"""
10+
Retrieves the provided data-source service.
11+
12+
:returns: The service.
13+
:rtype: LocationService
14+
"""
15+
return data_sources.get(source.lower())
Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from .coordinates import Coordinates
2-
from .utils import countrycodes
1+
from ..coordinates import Coordinates
2+
from ..utils import countrycodes
33

44
class Location:
55
"""
@@ -13,51 +13,77 @@ def __init__(self, id, country, province, coordinates, confirmed, deaths, recove
1313
self.province = province.strip()
1414
self.coordinates = coordinates
1515

16-
# Data.
16+
# Statistics.
1717
self.confirmed = confirmed
1818
self.deaths = deaths
1919
self.recovered = recovered
20-
21-
20+
2221
@property
2322
def country_code(self):
2423
"""
2524
Gets the alpha-2 code represention of the country. Returns 'XX' if none is found.
2625
"""
2726
return (countrycodes.country_code(self.country) or countrycodes.default_code).upper()
2827

29-
def serialize(self, timelines = False):
28+
def serialize(self):
3029
"""
3130
Serializes the location into a dict.
3231
33-
:param timelines: Whether to include the timelines.
3432
:returns: The serialized location.
3533
:rtype: dict
3634
"""
37-
serialized = {
35+
return {
3836
# General info.
3937
'id' : self.id,
4038
'country' : self.country,
41-
'province' : self.province,
4239
'country_code': self.country_code,
40+
'province' : self.province,
4341

4442
# Coordinates.
4543
'coordinates': self.coordinates.serialize(),
4644

47-
# Latest data.
45+
# Latest data (statistics).
4846
'latest': {
49-
'confirmed': self.confirmed.latest,
50-
'deaths' : self.deaths.latest,
51-
'recovered': self.recovered.latest
47+
'confirmed': self.confirmed,
48+
'deaths' : self.deaths,
49+
'recovered': self.recovered
5250
},
5351
}
5452

53+
class TimelinedLocation(Location):
54+
"""
55+
A location with timelines.
56+
"""
57+
58+
def __init__(self, id, country, province, coordinates, timelines):
59+
super().__init__(
60+
# General info.
61+
id, country, province, coordinates,
62+
63+
# Statistics (retrieve latest from timelines).
64+
confirmed=timelines.get('confirmed').latest,
65+
deaths=timelines.get('deaths').latest,
66+
recovered=timelines.get('recovered').latest,
67+
)
68+
69+
# Set timelines.
70+
self.timelines = timelines
71+
72+
def serialize(self, timelines = False):
73+
"""
74+
Serializes the location into a dict.
75+
76+
:param timelines: Whether to include the timelines.
77+
:returns: The serialized location.
78+
:rtype: dict
79+
"""
80+
serialized = super().serialize()
81+
5582
# Whether to include the timelines or not.
5683
if timelines:
5784
serialized.update({ 'timelines': {
58-
'confirmed': self.confirmed.serialize(),
59-
'deaths' : self.deaths.serialize(),
60-
'recovered': self.recovered.serialize(),
85+
# Serialize all the timelines.
86+
key: value.serialize() for (key, value) in self.timelines.items()
6187
}})
6288

6389
# Return the serialized location.

app/routes/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from flask import Blueprint, redirect, current_app as app
1+
from flask import Blueprint, redirect, request, current_app as app
2+
from ..data import data_source
23

34
# Follow the import order to avoid circular dependency
45
api_v1 = Blueprint('api_v1', __name__, url_prefix='')
@@ -14,3 +15,16 @@
1415
@app.route('/')
1516
def index():
1617
return redirect('https://github.com/ExpDev07/coronavirus-tracker-api', 302)
18+
19+
# Middleware for picking data source.
20+
@api_v2.before_request
21+
def datasource():
22+
"""
23+
Attaches the datasource to the request.
24+
"""
25+
# Retrieve the datas ource from query param.
26+
source = request.args.get('source', type=str, default='jhu')
27+
28+
# Attach source to request and return it.
29+
request.source = data_source(source)
30+
pass

app/routes/v2/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-

app/routes/v2/latest.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1-
from flask import jsonify
1+
from flask import request, jsonify
22
from ...routes import api_v2 as api
3-
from ...services import jhu
43

54
@api.route('/latest')
65
def latest():
76
# Get the serialized version of all the locations.
8-
locations = [ location.serialize() for location in jhu.get_all() ]
7+
locations = request.source.get_all()
98

109
# All the latest information.
11-
latest = list(map(lambda location: location['latest'], locations))
10+
# latest = list(map(lambda location: location['latest'], locations))
1211

1312
return jsonify({
1413
'latest': {
15-
'confirmed': sum(map(lambda latest: latest['confirmed'], latest)),
16-
'deaths' : sum(map(lambda latest: latest['deaths'], latest)),
17-
'recovered': sum(map(lambda latest: latest['recovered'], latest)),
14+
'confirmed': sum(map(lambda location: location.confirmed, locations)),
15+
'deaths' : sum(map(lambda location: location.deaths, locations)),
16+
'recovered': sum(map(lambda location: location.recovered, locations)),
1817
}
1918
})

app/routes/v2/locations.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from flask import jsonify, request
22
from distutils.util import strtobool
33
from ...routes import api_v2 as api
4-
from ...services import jhu
54

65
@api.route('/locations')
76
def locations():
@@ -10,7 +9,7 @@ def locations():
109
country_code = request.args.get('country_code', type=str)
1110

1211
# Retrieve all the locations.
13-
locations = jhu.get_all()
12+
locations = request.source.get_all()
1413

1514
# Filtering my country code if provided.
1615
if not country_code is None:
@@ -30,5 +29,5 @@ def location(id):
3029

3130
# Return serialized location.
3231
return jsonify({
33-
'location': jhu.get(id).serialize(timelines)
32+
'location': request.source.get(id).serialize(timelines)
3433
})

app/services/location/jhu.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from . import LocationService
2-
from ...location import Location
2+
from ...location import TimelinedLocation
33
from ...coordinates import Coordinates
44
from ...timeline import Timeline
55

@@ -16,6 +16,8 @@ def get(self, id):
1616
# Get location at the index equal to provided id.
1717
return self.get_all()[id]
1818

19+
# ---------------------------------------------------------------
20+
1921
import requests
2022
import csv
2123
from datetime import datetime
@@ -121,15 +123,17 @@ def get_locations():
121123
# Grab coordinates.
122124
coordinates = location['coordinates']
123125

124-
# Create location and append.
125-
locations.append(Location(
126+
# Create location (supporting timelines) and append.
127+
locations.append(TimelinedLocation(
126128
# General info.
127129
index, location['country'], location['province'], Coordinates(coordinates['lat'], coordinates['long']),
128130

129131
# Timelines (parse dates as ISO).
130-
Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }),
131-
Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }),
132-
Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() })
132+
{
133+
'confirmed': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }),
134+
'deaths' : Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }),
135+
'recovered': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() })
136+
}
133137
))
134138

135139
# Finally, return the locations.

tests/test_location.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def mocked_timeline(*args, **kwargs):
66
class TestTimeline:
77
def __init__(self, latest):
88
self.latest = latest
9-
9+
1010
return TestTimeline(args[0])
1111

1212
@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
1919
longitude, confirmed_latest, deaths_latest, recovered_latest):
2020

2121
# id, country, province, coordinates, confirmed, deaths, recovered
22-
coordinate = coordinates.Coordinates(latitude=latitude, longitude=longitude)
22+
coords = coordinates.Coordinates(latitude=latitude, longitude=longitude)
23+
24+
# Timelines
2325
confirmed = timeline.Timeline(confirmed_latest)
2426
deaths = timeline.Timeline(deaths_latest)
2527
recovered = timeline.Timeline(recovered_latest)
2628

27-
location_obj = location.Location(test_id, country, province, coordinate,
28-
confirmed, deaths, recovered)
29+
# Location.
30+
location_obj = location.TimelinedLocation(test_id, country, province, coords, {
31+
'confirmed': confirmed,
32+
'deaths' : deaths,
33+
'recovered': recovered,
34+
})
2935

3036
assert location_obj.country_code == country_code
3137

3238
#validate serialize
33-
check_dict = {'id': test_id,
34-
'country': country,
35-
'province': province,
36-
'country_code': country_code,
37-
'coordinates': {'latitude': latitude,
38-
'longitude': longitude},
39-
'latest': {'confirmed': confirmed_latest,
40-
'deaths': deaths_latest,
41-
'recovered': recovered_latest}}
39+
check_dict = {
40+
'id': test_id,
41+
'country': country,
42+
'country_code': country_code,
43+
'province': province,
44+
'coordinates': {
45+
'latitude': latitude,
46+
'longitude': longitude
47+
},
48+
'latest': {
49+
'confirmed': confirmed_latest,
50+
'deaths': deaths_latest,
51+
'recovered': recovered_latest
52+
}
53+
}
4254

4355
assert location_obj.serialize() == check_dict

0 commit comments

Comments
 (0)