Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,55 @@ Exclude timelines.
GET /v2/locations?timelines=0
```

### Getting US per county information.
```http
GET /v2/locations?source=csbs
```
```json
{
"Iowa":[
{
"coordinates":{
"latitude":41.67149574,
"longitude":-91.58805417
},
"county":"Johnson",
"latest":{
"confirmed":22,
"death":0,
"new":0
}
},
{
"coordinates":{
"latitude":41.68541161,
"longitude":-93.57344237
},
"county":"Polk",
"latest":{
"confirmed":6,
"death":0,
"new":0
}
},
...
...
}
```
Additionally, you can also filter by state.
```http
GET /v2/locations?source=csbs&state=Iowa
```

## Data

The data comes from the [2019 Novel Coronavirus (nCoV) Data Repository, provided
by JHU CCSE](https://github.com/CSSEGISandData/2019-nCoV). It is
programmatically retrieved, re-formatted and stored in the cache for one hour.

US County data comes from CSBS (https://www.csbs.org/information-covid-19-coronavirus)
It is programmatically retrieved, re-formatted and stored in the cache for one hour.

## Wrappers

These are the available API wrappers created by the community. They are not neccecarily maintained by any of this project's authors or contributors.
Expand Down
31 changes: 31 additions & 0 deletions app/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,35 @@ def serialize(self, timelines = False):
}})

# Return the serialized location.
return serialized

class LocationCounty:
"""
A county in the United States affected by coronavirus
"""
def __init__(self, county, coordinates, confirmed, new, death):
self.county = county
self.coordinates = coordinates
self.confirmed = confirmed
self.new = new
self.death = death

def serialize(self):
"""
Serializes the LocationCounty into a dict

:returns: The serialized LocationCounty
:rtype: dict
"""
serialized = {
'county': self.county,

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

'latest': {
'confirmed': self.confirmed,
'new': self.new,
'death': self.death
}
}
return serialized
58 changes: 43 additions & 15 deletions app/routes/v2/locations.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,55 @@
from flask import jsonify, request
from distutils.util import strtobool
from ...routes import api_v2 as api
from ...services import jhu
from ...services import jhu, csbs

@api.route('/locations')
def locations():
# Query parameters.
timelines = strtobool(request.args.get('timelines', default='0'))
country_code = request.args.get('country_code', type=str)
# Query parameter -> data source.
source = request.args.get('source', default='jhu')

# Retrieve all the locations.
locations = jhu.get_all()
if source == 'csbs':
# Query parameters
state = request.args.get('state', type=str, default='')

# Retrieve all locations.
data = csbs.get_all()
locations = data.copy()

# Filtering my country code if provided.
if not country_code is None:
locations = list(filter(lambda location: location.country_code == country_code.upper(), locations))
# Filter by state if applicable
if state in locations:
locations = locations[state]
return jsonify({
state: [
location.serialize() for location in locations
]
})

# serialize everything
for state in locations:
locations[state] = [county.serialize() for county in locations[state]]

return jsonify(locations)
else:
# Query parameters
timelines = strtobool(request.args.get('timelines', default='0'))
country_code = request.args.get('country_code', type=str)

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

# Serialize each location and return.
return jsonify({
'locations': [
location.serialize(timelines) for location in locations
]
})
# 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(timelines) for location in locations
]
})



@api.route('/locations/<int:id>')
def location(id):
Expand Down
4 changes: 3 additions & 1 deletion app/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from .location.jhu import JhuLocationService
from .location.csbs import CSBSLocationService

# Instances of the services.
jhu = JhuLocationService()
jhu = JhuLocationService()
csbs = CSBSLocationService()
56 changes: 56 additions & 0 deletions app/services/location/csbs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from . import LocationService
from ...location import LocationCounty
from ...coordinates import Coordinates

class CSBSLocationService(LocationService):
"""
Servive for retrieving locations from csbs
"""

def get_all(self):
# Get the locations
return get_locations()

def get(self, state):
return self.get_all()[state]

import requests
import csv
from datetime import datetime
from cachetools import cached, TTLCache

# Base URL for fetching data
base_url = 'https://facts.csbs.org/covid-19/covid19_county.csv'

@cached(cache=TTLCache(maxsize=1, ttl=3600))
def get_locations():
"""
Retrieves county locations; locations are cached for 1 hour

:returns: The locations.
:rtype: dict
"""
request = requests.get(base_url)
text = request.text

data = list(csv.DictReader(text.splitlines()))

locations = {}

for item in data:
state = item['State Name']
if state not in locations:
locations[state] = []

county = item['County Name']
if county == "Unassigned" or county == "Unknown":
continue

confirmed = int(item['Confirmed'] or 0)
new = int(item['New'] or 0)
death = int(item['Death'] or 0)
coordinates = Coordinates(float(item['Latitude']), float(item['Longitude']))
location_county = LocationCounty(county, coordinates, confirmed, new, death)
locations[state].append(location_county)

return locations
33 changes: 33 additions & 0 deletions tests/example_data/sample_covid19_county.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
County Name,State Name,Confirmed,New,Death,Fatality Rate,Latitude,Longitude,Last Update
New York,New York,4408,454,26,0.6%,40.71455,-74.00714,2020-03-20 13:58 EDT
Westchester,New York,1091,293,0,0%,41.16319759,-73.7560629,2020-03-20 13:58 EDT
Nassau,New York,754,382,4,0.5%,40.74165225,-73.58899619,2020-03-20 13:58 EDT
Yakima,Washington,7,0,0,0%,46.60448,-120.50721,2020-03-20 13:58 EDT
Thurston,Washington,6,0,0,0%,46.91980578,-122.8298691,2020-03-20 13:58 EDT
Jefferson,Washington,4,0,0,0%,47.74810608,-123.6000095,2020-03-20 13:58 EDT
Douglas,Kansas,1,0,0,0%,38.88462907,-95.29255463,2020-03-20 13:58 EDT
Cherokee,Kansas,1,0,0,0%,37.16926692,-94.8462675759999,2020-03-20 13:58 EDT
Jackson,Kansas,1,0,0,0%,39.4168027220001,-95.793674403,2020-03-20 13:58 EDT
Twin Falls,Idaho,1,0,0,0%,42.55619,-114.4696,2020-03-20 13:58 EDT
Kootenai,Idaho,1,0,0,0%,47.6775872760001,-116.697131928,2020-03-20 13:58 EDT
Chittenden,Vermont,4,0,1,25%,44.45799511,-73.05404973,2020-03-20 13:58 EDT
Bennington,Vermont,3,0,0,0%,42.87672,-73.19818,2020-03-20 13:58 EDT
Windsor,Vermont,3,0,1,33.3%,43.48115,-72.38581,2020-03-20 13:58 EDT
Washington,Vermont,1,0,0,0%,44.27344561,-72.61485925,2020-03-20 13:58 EDT
Orange,Vermont,1,0,0,0%,44.14854,-72.40233,2020-03-20 13:58 EDT
Addison,Vermont,1,0,0,0%,44.0280736,-73.13152876,2020-03-20 13:58 EDT
Burleigh,North Dakota,11,0,0,0%,46.97801044,-100.4669442,2020-03-20 13:58 EDT
Tucker,West Virginia,2,0,0,0%,39.1135508250001,-79.56492129,2020-03-20 13:58 EDT
Mercer,West Virginia,1,0,0,0%,37.40556515,-81.11143231,2020-03-20 13:58 EDT
Monongalia,West Virginia,1,0,0,0%,39.630233859,-80.0465546289999,2020-03-20 13:58 EDT
Unassigned,New York,166,149,4,2.4%,42.165726,-74.948051,2020-03-20 13:58 EDT
Unassigned,Washington,151,0,0,0%,47.400902,-121.490494,2020-03-20 13:58 EDT
Unassigned,Colorado,57,0,0,0%,39.059811,-105.311104,2020-03-20 13:58 EDT
Unknown,Pennsylvania,55,55,0,0%,40.590752,-77.209755,2020-03-20 13:58 EDT
Unassigned,Pennsylvania,0,0,0,NaN%,40.590752,-77.209755,2020-03-20 13:58 EDT
Franklin,Pennsylvania,1,1,0,0%,39.927495836,-77.721161869,2020-03-20 13:58 EDT
Franklin,North Carolina,4,4,0,0%,36.0827448150001,-78.285600305,2020-03-20 13:58 EDT
Lee,North Carolina,1,1,0,0%,35.475059921,-79.17154054,2020-03-20 13:58 EDT
Clay,Minnesota,1,1,0,0%,46.892347886,-96.490737839,2020-03-20 13:58 EDT
Yuma,Arizona,1,1,0,0%,32.768956524,-113.905830295,2020-03-20 13:58 EDT
Dunklin,Missouri,1,1,0,0%,36.105848973,-90.16563,2020-03-20 13:58 EDT
38 changes: 38 additions & 0 deletions tests/test_csbs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import app
import datetime
import pytest
from unittest import mock
from app.services.location import csbs

def mocked_csbs_requests_get(*args, **kwargs):
class FakeRequestsGetResponse:
"""
Returns instance of `FakeRequestsGetResponse`
when calling `app.services.location.csbs.requests.get()
"""
def __init__(self):
self.text = self.read_file()

def read_file(self):
"""
Mock HTTP GET-method and return text from file
"""
filepath = "tests/example_data/sample_covid19_county.csv"
print("Try to read {}".format(filepath))
with open(filepath, "r") as file:
return file.read()

return FakeRequestsGetResponse()

@mock.patch('app.services.location.csbs.requests.get', side_effect=mocked_csbs_requests_get)
def test_get_locations(mock_request_get):
data = csbs.get_locations()
assert isinstance(data, dict)
assert isinstance(data["Washington"], list)
assert data.get("Wisconsin") == None

# check to see that Unknown/Unassigned has been filtered
for state in data:
for county in data[state]:
assert county.county != "Unknown"
assert county.county != "Unassigned"