Skip to content

Commit d0fef4f

Browse files
committed
Added ability to use filters for aggregate data
2 parents 89ea4ff + 86cdbdb commit d0fef4f

File tree

12 files changed

+219
-115
lines changed

12 files changed

+219
-115
lines changed

README.md

Lines changed: 14 additions & 1 deletion
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
@@ -41,6 +51,7 @@ GET /v2/locations
4151
"country": "Thailand",
4252
"country_code": "TH",
4353
"province": "",
54+
"last_updated": "2020-03-21T06:59:11.315422Z",
4455
"coordinates": {
4556
"latitude": "15",
4657
"longitude": "101"
@@ -56,6 +67,7 @@ GET /v2/locations
5667
"country": "Norway",
5768
"country_code": "NO",
5869
"province": "",
70+
"last_updated": "2020-03-21T06:59:11.315422Z",
5971
"coordinates": {
6072
"latitude": "60.472",
6173
"longitude": "8.4689"
@@ -91,6 +103,7 @@ GET /v2/locations/:id
91103
"country": "Norway",
92104
"country_code": "NO",
93105
"province": "",
106+
"last_updated": "2020-03-21T06:59:11.315422Z",
94107
"coordinates": { },
95108
"latest": { },
96109
"timelines": {
@@ -121,7 +134,7 @@ programmatically retrieved, re-formatted and stored in the cache for one hour.
121134

122135
## Wrappers
123136

124-
These are the available API wrappers created by the community. They are not neccecarily maintained by any of this project's authors or contributors.
137+
These are the available API wrappers created by the community. They are not necessarily maintained by any of this project's authors or contributors.
125138

126139
### C#
127140

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())

app/location.py

Lines changed: 0 additions & 64 deletions
This file was deleted.

app/location/__init__.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from ..coordinates import Coordinates
2+
from ..utils import countrycodes
3+
4+
class Location:
5+
"""
6+
A location in the world affected by the coronavirus.
7+
"""
8+
9+
def __init__(self, id, country, province, coordinates, last_updated, confirmed, deaths, recovered):
10+
# General info.
11+
self.id = id
12+
self.country = country.strip()
13+
self.province = province.strip()
14+
self.coordinates = coordinates
15+
16+
# Last update.
17+
self.last_updated = last_updated
18+
19+
# Statistics.
20+
self.confirmed = confirmed
21+
self.deaths = deaths
22+
self.recovered = recovered
23+
24+
@property
25+
def country_code(self):
26+
"""
27+
Gets the alpha-2 code represention of the country. Returns 'XX' if none is found.
28+
"""
29+
return (countrycodes.country_code(self.country) or countrycodes.default_code).upper()
30+
31+
def serialize(self):
32+
"""
33+
Serializes the location into a dict.
34+
35+
:returns: The serialized location.
36+
:rtype: dict
37+
"""
38+
return {
39+
# General info.
40+
'id' : self.id,
41+
'country' : self.country,
42+
'country_code': self.country_code,
43+
'province' : self.province,
44+
45+
# Coordinates.
46+
'coordinates': self.coordinates.serialize(),
47+
48+
# Last updated.
49+
'last_updated': self.last_updated,
50+
51+
# Latest data (statistics).
52+
'latest': {
53+
'confirmed': self.confirmed,
54+
'deaths' : self.deaths,
55+
'recovered': self.recovered
56+
},
57+
}
58+
59+
class TimelinedLocation(Location):
60+
"""
61+
A location with timelines.
62+
"""
63+
64+
def __init__(self, id, country, province, coordinates, last_updated, timelines):
65+
super().__init__(
66+
# General info.
67+
id, country, province, coordinates, last_updated,
68+
69+
# Statistics (retrieve latest from timelines).
70+
confirmed=timelines.get('confirmed').latest or 0,
71+
deaths=timelines.get('deaths').latest or 0,
72+
recovered=timelines.get('recovered').latest or 0,
73+
)
74+
75+
# Set timelines.
76+
self.timelines = timelines
77+
78+
def serialize(self, timelines = False):
79+
"""
80+
Serializes the location into a dict.
81+
82+
:param timelines: Whether to include the timelines.
83+
:returns: The serialized location.
84+
:rtype: dict
85+
"""
86+
serialized = super().serialize()
87+
88+
# Whether to include the timelines or not.
89+
if timelines:
90+
serialized.update({ 'timelines': {
91+
# Serialize all the timelines.
92+
key: value.serialize() for (key, value) in self.timelines.items()
93+
}})
94+
95+
# Return the serialized location.
96+
return serialized

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: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
from flask import jsonify, request
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():
7-
#Query parameters.
8-
country_code = request.args.get('country_code', type=str)
6+
# Query parameters.
7+
args = request.args
98

109
# Get the serialized version of all the locations.
11-
locations = [ location.serialize() for location in jhu.get_all() ]
10+
locations = request.source.get_all()
11+
#print([i.country_code for i in locations])
1212

13-
# Return aggregate data for country if provided.
14-
if not country_code is None:
15-
locations = list(filter(lambda location: location['country_code'] == country_code.upper(), locations))
13+
# Filter based on args.
14+
if len(args) > 0:
15+
locations = [i for i in locations for j in args if hasattr(i, j) and getattr(i, j) == args[j]]
1616

1717
# All the latest information.
18-
latest = list(map(lambda location: location['latest'], locations))
18+
# latest = list(map(lambda location: location['latest'], locations))
1919

2020
return jsonify({
2121
'latest': {
22-
'confirmed': sum(map(lambda latest: latest['confirmed'], latest)),
23-
'deaths' : sum(map(lambda latest: latest['deaths'], latest)),
24-
'recovered': sum(map(lambda latest: latest['recovered'], latest)),
22+
'confirmed': sum(map(lambda location: location.confirmed, locations)),
23+
'deaths' : sum(map(lambda location: location.deaths, locations)),
24+
'recovered': sum(map(lambda location: location.recovered, locations)),
2525
}
2626
})

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: 20 additions & 7 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,26 @@ 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.
127-
index, location['country'], location['province'], Coordinates(coordinates['lat'], coordinates['long']),
129+
index, location['country'], location['province'],
130+
131+
# Coordinates.
132+
Coordinates(
133+
coordinates['lat'],
134+
coordinates['long']
135+
),
136+
137+
# Last update.
138+
datetime.utcnow().isoformat() + 'Z',
128139

129140
# 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() })
141+
{
142+
'confirmed': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }),
143+
'deaths' : Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }),
144+
'recovered': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() })
145+
}
133146
))
134147

135148
# Finally, return the locations.

0 commit comments

Comments
 (0)