diff --git a/README.md b/README.md index b886d87a..d4d6b199 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ We provide multiple data-sources you can pick from, simply add the query paramat * **jhu** - https://github.com/CSSEGISandData/COVID-19 - Data repository operated by the Johns Hopkins University Center for Systems Science and Engineering (JHU CSSE). +* **csbs** - https://www.csbs.org/information-covid-19-coronavirus - US County data comes from Conference of State Bank Supervisors + * **... more to come later**. ### Getting latest amount of total confirmed cases, deaths, and recoveries. @@ -126,11 +128,53 @@ Exclude timelines. GET /v2/locations?timelines=0 ``` -## 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. +### Getting US per county information. +```http +GET /v2/locations?source=csbs +``` +```json +{ + "locations": [ + { + "coordinates": { + "latitude": 40.71455, + "longitude": -74.00714 + }, + "country": "US", + "country_code": "US", + "county": "New York", + "id": 0, + "last_updated": "2020-03-21T14:00:00Z", + "latest": { + "confirmed": 6211, + "deaths": 43, + "recovered": 0 + }, + "province": "New York", + "state": "New York" + }, + { + "coordinates": { + "latitude": 41.16319759, + "longitude": -73.7560629 + }, + "country": "US", + "country_code": "US", + "county": "Westchester", + "id": 1, + "last_updated": "2020-03-21T14:00:00Z", + "latest": { + "confirmed": 1385, + "deaths": 0, + "recovered": 0 + }, + "province": "Westchester", + "state": "New York" + }, + ... + ] +} +``` ## Wrappers diff --git a/app/data/__init__.py b/app/data/__init__.py index 518737b3..0d11f7b1 100644 --- a/app/data/__init__.py +++ b/app/data/__init__.py @@ -1,8 +1,10 @@ from ..services.location.jhu import JhuLocationService +from ..services.location.csbs import CSBSLocationService # Mapping of services to data-sources. data_sources = { 'jhu': JhuLocationService(), + 'csbs': CSBSLocationService() } def data_source(source): diff --git a/app/location/__init__.py b/app/location/__init__.py index 70f9464b..6fd8cd23 100644 --- a/app/location/__init__.py +++ b/app/location/__init__.py @@ -92,5 +92,35 @@ def serialize(self, timelines = False): key: value.serialize() for (key, value) in self.timelines.items() }}) + # Return the serialized location. + return serialized + +class CSBSLocation(Location): + """ + A CSBS (county) location. + """ + def __init__(self, id, state, county, coordinates, last_updated, confirmed, deaths): + super().__init__( + id, 'US', county, coordinates, last_updated, confirmed, deaths, recovered=0 + ) + + self.state = state + self.county = county + + def serialize(self, timelines=False): + """ + Serializes the location into a dict. + + :returns: The serialized location. + :rtype: dict + """ + serialized = super().serialize() + + # Update with new fields. + serialized.update({ + 'state': self.state, + 'county': self.county, + }) + # Return the serialized location. return serialized \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py index 0c5b206c..4031209d 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -1,4 +1,6 @@ from .location.jhu import JhuLocationService +from .location.csbs import CSBSLocationService # Instances of the services. -jhu = JhuLocationService() \ No newline at end of file +jhu = JhuLocationService() +csbs = CSBSLocationService() \ No newline at end of file diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py new file mode 100644 index 00000000..0ed239a3 --- /dev/null +++ b/app/services/location/csbs.py @@ -0,0 +1,65 @@ +from . import LocationService +from ...location import CSBSLocation +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, id): + return self.get_all()[id] + +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 i, item in enumerate(data): + state = item['State Name'] + county = item['County Name'] + if county == "Unassigned" or county == "Unknown": + continue + + confirmed = int(item['Confirmed'] or 0) + death = int(item['Death'] or 0) + coordinates = Coordinates(float(item['Latitude']), float(item['Longitude'])) + + # Parse time to ISO format + last_update = item['Last Update'] + date = last_update.split("-") + year = int(date[0]) + month = int(date[1]) + date = date[2].split(" ") + day = int(date[0]) + time = date[1].split(":") + hour = int(time[0]) + minute = int(time[1]) + d = datetime(year=year, month=month, day=day, hour=hour, minute=minute) + last_update = d.isoformat() + 'Z' + + locations.append(CSBSLocation(i, state, county, coordinates, last_update, confirmed, death)) + + return locations diff --git a/tests/example_data/sample_covid19_county.csv b/tests/example_data/sample_covid19_county.csv new file mode 100644 index 00000000..ee972c59 --- /dev/null +++ b/tests/example_data/sample_covid19_county.csv @@ -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 diff --git a/tests/test_csbs.py b/tests/test_csbs.py new file mode 100644 index 00000000..3eaab01c --- /dev/null +++ b/tests/test_csbs.py @@ -0,0 +1,35 @@ +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, list) + + # check to see that Unknown/Unassigned has been filtered + for d in data: + assert d.county != "Unknown" + assert d.county != "Unassigned" \ No newline at end of file