Skip to content
Closed
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
3 changes: 1 addition & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from flask import Flask
from flask_cors import CORS
from . import settings

def create_app():
"""
Expand All @@ -12,7 +11,7 @@ def create_app():
CORS(app)

# Set app config from settings.
app.config.from_pyfile('settings.py');
app.config.from_pyfile('config/settings.py');

with app.app_context():
# Import routes.
Expand Down
File renamed without changes.
20 changes: 20 additions & 0 deletions app/coordinates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class Coordinates:
"""
A position on earth using decimal coordinates (latitude and longitude).
"""

def __init__(self, latitude, longitude):
self.latitude = latitude
self.longitude = longitude

def serialize(self):
"""
Serializes the coordinates into a dict.
"""
return {
'latitude' : self.latitude,
'longitude': self.longitude
}

def __str__(self):
return 'lat: %s, long: %s' % (self.latitude, self.longitude)
49 changes: 49 additions & 0 deletions app/location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from .coordinates import Coordinates
from .utils import countrycodes

class Location:
"""
A location in the world affected by the coronavirus.
"""

def __init__(self, id, country, province, coordinates, confirmed, deaths, recovered):
# General info.
self.id = id
self.country = country.strip()
self.province = province.strip()
self.coordinates = coordinates

# Data.
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):
"""
Serializes the location into a dict.
"""
return {
# General info.
'id' : self.id,
'country' : self.country,
'province' : self.province,
'country_code': self.country_code,

# Coordinates.
'coordinates': self.coordinates.serialize(),

# Latest data.
'latest': {
'confirmed': self.confirmed.latest,
'deaths' : self.deaths.latest,
'recovered': self.recovered.latest
}
}
4 changes: 0 additions & 4 deletions app/models/location.py

This file was deleted.

9 changes: 5 additions & 4 deletions app/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import confirmed
from . import deaths
from . import recovered
from . import all
# API version 1.
from .v1 import confirmed, deaths, recovered, all

# API version 2.
from .v2 import locations, latest
8 changes: 4 additions & 4 deletions app/routes/all.py → app/routes/v1/all.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from flask import jsonify
from flask import current_app as app
from ..data import get_data
from ...services.location.jhu import get_category

@app.route('/all')
def all():
# Get all the categories.
confirmed = get_data('confirmed')
deaths = get_data('deaths')
recovered = get_data('recovered')
confirmed = get_category('confirmed')
deaths = get_category('deaths')
recovered = get_category('recovered')

return jsonify({
# Data.
Expand Down
4 changes: 2 additions & 2 deletions app/routes/confirmed.py → app/routes/v1/confirmed.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import jsonify
from flask import current_app as app
from ..data import get_data
from ...services.location.jhu import get_category

@app.route('/confirmed')
def confirmed():
return jsonify(get_data('confirmed'))
return jsonify(get_category('confirmed'))
4 changes: 2 additions & 2 deletions app/routes/deaths.py → app/routes/v1/deaths.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import jsonify
from flask import current_app as app
from ..data import get_data
from ...services.location.jhu import get_category

@app.route('/deaths')
def deaths():
return jsonify(get_data('deaths'))
return jsonify(get_category('deaths'))
4 changes: 2 additions & 2 deletions app/routes/recovered.py → app/routes/v1/recovered.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import jsonify
from flask import current_app as app
from ..data import get_data
from ...services.location.jhu import get_category

@app.route('/recovered')
def recovered():
return jsonify(get_data('recovered'))
return jsonify(get_category('recovered'))
18 changes: 18 additions & 0 deletions app/routes/v2/latest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from flask import jsonify, current_app as app
from ...services import jhu

@app.route('/v2/latest')
def latest():
# Get the serialized version of all the locations.
locations = [ location.serialize() for location in jhu.get_all() ]

# All the latest information.
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)),
}
})
40 changes: 40 additions & 0 deletions app/routes/v2/locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from flask import jsonify, request, current_app as app
from ...services import jhu

@app.route('/v2/locations')
def locations():
# Query parameters.
country_code = request.args.get('country_code', type=str)

# Retrieve all the locations.
locations = jhu.get_all()

# Filtering my country code if provided.
if not country_code is None:
locations = list(filter(lambda location: location.country_code == country_code.upper(), locations))

# Serialize each location and return.
return jsonify({
'locations': [
location.serialize() for location in locations
]
})

@app.route('/v2/locations/<int:id>')
def location(id):
# Retrieve location with the provided id.
location = jhu.get(id)

# Get all the timelines.
timelines = {
'confirmed': location.confirmed.serialize(),
'deaths' : location.deaths.serialize(),
'recovered': location.recovered.serialize(),
}

# Serialize the location, add timelines, and then return.
return jsonify({
'location': {
**jhu.get(id).serialize(), **{ 'timelines': timelines }
}
})
4 changes: 4 additions & 0 deletions app/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .location.jhu import JhuLocationService

# Instances of the services.
jhu = JhuLocationService()
26 changes: 26 additions & 0 deletions app/services/location/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from abc import ABC, abstractmethod

class LocationService(ABC):
"""
Service for retrieving locations.
"""

@abstractmethod
def get_all(self):
"""
Gets and returns all of the locations.

:returns: The locations.
:rtype: Location
"""
raise NotImplementedError

@abstractmethod
def get(self, id):
"""
Gets and returns location with the provided id.

:returns: The location.
:rtype: Location
"""
raise NotImplementedError
57 changes: 51 additions & 6 deletions app/data/__init__.py → app/services/location/jhu.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
from . import LocationService
from ...location import Location
from ...coordinates import Coordinates
from ...timeline import Timeline

class JhuLocationService(LocationService):
"""
Service for retrieving locations from Johns Hopkins CSSE (https://github.com/CSSEGISandData/COVID-19).
"""

def get_all(self):
# Get all of the data categories locations.
confirmed = get_category('confirmed')['locations']
deaths = get_category('deaths')['locations']
recovered = get_category('recovered')['locations']

# Final locations to return.
locations = []

# Go through confirmed locations.
for index, location in enumerate(confirmed):
# Grab coordinates.
coordinates = location['coordinates']

# Create location and append.
locations.append(Location(
# General info.
index, location['country'], location['province'], Coordinates(coordinates['lat'], coordinates['long']),

# TODO: date key as ISO format.
# { datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': int(amount or 0) for date, amount in history.items() }

# Timelines.
Timeline(confirmed[index]['history']),
Timeline(deaths[index]['history']),
Timeline(recovered[index]['history'])
))

# Finally, return the locations.
return locations

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
from cachetools import cached, TTLCache
from ..utils import countrycodes, date as date_util
from ...utils import countrycodes, date as date_util

"""
Base URL for fetching data.
Base URL for fetching category.
"""
base_url = 'https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/time_series_19-covid-%s.csv';

@cached(cache=TTLCache(maxsize=1024, ttl=3600))
def get_data(category):
def get_category(category):
"""
Retrieves the data for the provided type. The data is cached for 1 hour.
Retrieves the data for the provided category. The data is cached for 1 hour.
"""

# Adhere to category naming standard.
Expand All @@ -39,7 +84,7 @@ def get_data(category):
country = item['Country/Region']

# Latest data insert value.
latest = list(history.values())[-1];
latest = list(sorted(history.values()))[-1];

# Normalize the item and append to locations.
locations.append({
Expand Down Expand Up @@ -70,4 +115,4 @@ def get_data(category):
'latest': latest,
'last_updated': datetime.utcnow().isoformat() + 'Z',
'source': 'https://github.com/ExpDev07/coronavirus-tracker-api',
}
}
33 changes: 33 additions & 0 deletions app/timeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from datetime import datetime
from collections import OrderedDict

class Timeline:
"""
Timeline with history of data.
"""

def __init__(self, history = {}):
self.__timeline = history

@property
def timeline(self):
"""
Gets the history sorted by date (key).
"""
return OrderedDict(sorted(self.__timeline.items()))

@property
def latest(self):
"""
Gets the latest available history value.
"""
return list(self.timeline.values())[-1] or 0

def serialize(self):
"""
Serializes the data into a dict.
"""
return {
'latest' : self.latest,
'timeline': self.timeline
}