diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..2d0d7eb9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +ko_fi: ExpDev +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/README.md b/README.md index 2c1b3304..840efc72 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -## Coronavirus Tracker API +

+ Coronavirus Tracker API +

+ Provides up-to-date data about Coronavirus outbreak. Includes numbers about confirmed cases, deaths and recovered. Support multiple data-sources. @@ -18,6 +21,10 @@ Support multiple data-sources. ![Covid-19 Recovered](https://covid19-badges.herokuapp.com/recovered/latest) ![Covid-19 Deaths](https://covid19-badges.herokuapp.com/deaths/latest) +## Recovered cases showing 0 + +**JHU (our main data provider) [no longer provides data for amount of recoveries](https://github.com/ExpDev07/coronavirus-tracker-api/issues/155), and as a result, the API will be showing 0 for this statistic. Apolegies for any inconvenience. Hopefully we'll be able to find an alternative data-source that offers this.** + ## Available data-sources: Currently 2 different data-sources are available to retrieve the data: @@ -28,7 +35,6 @@ Currently 2 different data-sources are available to retrieve the data: __jhu__ data-source will be used as a default source if you don't specify a *source parameter* in your request. - ## API Reference All endpoints are located at ``coronavirus-tracker-api.herokuapp.com/v2/`` and are accessible via https. For instance: you can get data per location by using this URL: @@ -40,7 +46,6 @@ You can open the URL in your browser to further inspect the response. Or you can curl https://coronavirus-tracker-api.herokuapp.com/v2/locations | json_pp ``` - ## API Endpoints ### Sources Endpoint diff --git a/app/main.py b/app/main.py index 5b62ac9c..8e0b367c 100644 --- a/app/main.py +++ b/app/main.py @@ -89,7 +89,7 @@ def get_latest(request: fastapi.Request, source: Sources = "jhu"): @V2.get( - "/locations", response_model=models.AllLocations, response_model_exclude_unset=True + "/locations", response_model=models.Locations, response_model_exclude_unset=True ) def get_all_locations( request: fastapi.Request, @@ -131,8 +131,9 @@ async def sources(): """ return {"sources": list(data_sources.keys())} - +# Include routers. APP.include_router(V2, prefix="/v2-beta", tags=["v2"]) + # mount the existing Flask app # v1 @ / # v2 @ /v2 diff --git a/app/models.py b/app/models.py index a67b7573..36b1396f 100644 --- a/app/models.py +++ b/app/models.py @@ -3,48 +3,45 @@ ~~~~~~~~~~~~~ Reponse data models. """ -import datetime as dt +from pydantic import BaseModel from typing import Dict, List -import pydantic - - -class Totals(pydantic.BaseModel): +class Totals(BaseModel): confirmed: int deaths: int recovered: int -class Latest(pydantic.BaseModel): +class Latest(BaseModel): latest: Totals -class TimelineStats(pydantic.BaseModel): +class TimelineStats(BaseModel): latest: int - timeline: Dict[str, int] + timeline: Dict[str, int] = {} -class TimelinedLocation(pydantic.BaseModel): +class TimelinedLocation(BaseModel): confirmed: TimelineStats deaths: TimelineStats recovered: TimelineStats -class Country(pydantic.BaseModel): - coordinates: Dict +class Country(BaseModel): + id: int country: str country_code: str - id: int - last_updated: dt.datetime - latest: Totals province: str = "" - timelines: TimelinedLocation = None # FIXME + last_updated: str # TODO use datetime.datetime type. + coordinates: Dict + latest: Totals + timelines: TimelinedLocation = {} -class AllLocations(pydantic.BaseModel): +class Locations(BaseModel): latest: Totals - locations: List[Country] + locations: List[Country] = [] -class Location(pydantic.BaseModel): +class Location(BaseModel): location: Country diff --git a/app/services/__init__.py b/app/services/__init__.py index 4031209d..e69de29b 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -1,6 +0,0 @@ -from .location.jhu import JhuLocationService -from .location.csbs import CSBSLocationService - -# Instances of the services. -jhu = JhuLocationService() -csbs = CSBSLocationService() \ No newline at end of file diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index f6140b75..b0f3a2e2 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -67,7 +67,7 @@ def get_locations(): ), # Last update (parse as ISO). - datetime.strptime(last_update, '%Y/%m/%d %H:%M').isoformat() + 'Z', + datetime.strptime(last_update, '%Y-%m-%d %H:%M').isoformat() + 'Z', # Statistics. int(item['Confirmed'] or 0), diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index af5c126e..0a42c9d9 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -27,7 +27,7 @@ def get(self, id): """ 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'; +base_url = 'https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/'; @cached(cache=TTLCache(maxsize=1024, ttl=3600)) def get_category(category): @@ -39,10 +39,20 @@ def get_category(category): """ # Adhere to category naming standard. - category = category.lower().capitalize(); + category = category.lower(); + + # URL to request data from. + url = base_url + 'time_series_covid19_%s_global.csv' % category + + # Different URL is needed for recoveries. + # Read about deprecation here: https://github.com/CSSEGISandData/COVID-19/tree/master/csse_covid_19_data/csse_covid_19_time_series. + if category == 'recovered': + url = base_url + 'time_series_19-covid-Recovered.csv' + + print (url) # Request the data - request = requests.get(base_url % category) + request = requests.get(url) text = request.text # Parse the CSV. @@ -106,7 +116,7 @@ def get_locations(): # Get all of the data categories locations. confirmed = get_category('confirmed')['locations'] deaths = get_category('deaths')['locations'] - recovered = get_category('recovered')['locations'] + # recovered = get_category('recovered')['locations'] # Final locations to return. locations = [] @@ -117,7 +127,7 @@ def get_locations(): timelines = { 'confirmed' : confirmed[index]['history'], 'deaths' : deaths[index]['history'], - 'recovered' : recovered[index]['history'], + # 'recovered' : recovered[index]['history'], } # Grab coordinates. @@ -141,7 +151,7 @@ def get_locations(): { 'confirmed': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['confirmed'].items() }), 'deaths' : Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['deaths'].items() }), - 'recovered': Timeline({ datetime.strptime(date, '%m/%d/%y').isoformat() + 'Z': amount for date, amount in timelines['recovered'].items() }) + 'recovered': Timeline({}) } )) diff --git a/app/timeline.py b/app/timeline.py index dc9adacd..44e54c12 100644 --- a/app/timeline.py +++ b/app/timeline.py @@ -21,7 +21,15 @@ def latest(self): """ Gets the latest available history value. """ - return list(self.timeline.values())[-1] or 0 + # Get values in a list. + values = list(self.timeline.values()) + + # Last item is the latest. + if len(values): + return values[-1] or 0 + + # Fallback value of 0. + return 0 def serialize(self): """ diff --git a/coronavirus-tracker-api-swagger.yaml b/coronavirus-tracker-api-swagger.yaml deleted file mode 100644 index d17b9d42..00000000 --- a/coronavirus-tracker-api-swagger.yaml +++ /dev/null @@ -1,152 +0,0 @@ -swagger: '2.0' -info: - description: 'Corona Virus Tracker API based on JHU data sets. https://github.com/ExpDev07/coronavirus-tracker-api' - version: 1.0.0 - title: Corona Virus Tracker API - contact: - url: https://github.com/ExpDev07/coronavirus-tracker-api -host: coronavirus-tracker-api.herokuapp.com -basePath: /v2 -schemes: - - https -paths: - /latest: - get: - summary: Latest global Corona stats - operationId: latest - produces: - - application/json - responses: - '200': - description: successful operation - schema: - type: object - properties: - latest: - type: object - properties: - confirmed: - type: number - example: 214910 - deaths: - type: number - example: 8733 - recovered: - type: number - example: 83207 - /locations: - get: - summary: All Locations - operationId: allLocations - produces: - - application/json - parameters: - - name: country_code - in: query - description: "ISO alpha 2 country code. https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2" - required: false - type: string - x-example: US - - name: timelines - in: query - description: "Include timeline objects in response? true or false" - required: false - type: string - x-example: true - responses: - '200': - description: successful operation - schema: - type: object - properties: - locations: - type: array - items: - $ref: '#/definitions/Location' - '/locations/{id}': - get: - summary: location - operationId: location - produces: - - application/json - parameters: - - name: id - in: path - description: "Internal ID" - required: true - type: string - x-example: 39 - responses: - '200': - description: successful operation - schema: - type: object - properties: - location: - $ref: '#/definitions/Location' - '404': - description: Location not found -definitions: - Location: - type: object - properties: - coordinates: - type: object - properties: - latitude: - type: string - example: "47.4009" - longitude: - type: string - example: "-121.4905" - country: - type: string - example: "US" - country_code: - type: string - example: US - id: - type: number - example: 98 - latest: - type: object - properties: - confirmed: - type: number - example: 1014 - deaths: - type: number - example: 55 - recovered: - type: number - example: 10 - province: - type: string - example: "Washington" - timelines: - type: object - properties: - confirmed: - type: object - properties: - latest: - type: number - example: 1014 - timeline: - type: object - deaths: - type: object - properties: - latest: - type: number - example: 55 - timeline: - type: object - recovered: - type: object - properties: - latest: - type: number - example: 10 - timeline: - type: object diff --git a/tests/example_data/sample_covid19_county.csv b/tests/example_data/sample_covid19_county.csv index 9f89341d..ee972c59 100644 --- a/tests/example_data/sample_covid19_county.csv +++ b/tests/example_data/sample_covid19_county.csv @@ -1,33 +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 +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/example_data/time_series_19-covid-Confirmed.csv b/tests/example_data/time_series_covid19_confirmed_global.csv similarity index 100% rename from tests/example_data/time_series_19-covid-Confirmed.csv rename to tests/example_data/time_series_covid19_confirmed_global.csv diff --git a/tests/example_data/time_series_19-covid-Deaths.csv b/tests/example_data/time_series_covid19_deaths_global.csv similarity index 100% rename from tests/example_data/time_series_19-covid-Deaths.csv rename to tests/example_data/time_series_covid19_deaths_global.csv diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 7aba5f74..a503a1c2 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -24,7 +24,15 @@ def read_file(self, state): """ Mock HTTP GET-method and return text from file """ - filepath = "tests/example_data/time_series_19-covid-{}.csv".format(state) + state = state.lower() + + # Determine filepath. + filepath = "tests/example_data/{}.csv".format(state) + + if state == 'recovered': + filepath = 'tests/example_data/time_series_19-covid-Recovered.csv' + + # Return fake response. print("Try to read {}".format(filepath)) with open(filepath, "r") as file: return file.read() @@ -58,76 +66,6 @@ def isoformat(self): return DateTimeStrpTime(date, strformat) -@pytest.mark.parametrize("category, capitalize_category", [ - ("deaths", "Deaths"), - ("recovered", "Recovered"), - ("confirmed", "Confirmed")]) -@mock.patch('app.services.location.jhu.requests.get', side_effect=mocked_requests_get) -def test_validate_category(mock_request_get, category, capitalize_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' - request = app.services.location.jhu.requests.get(base_url % category) - - assert request.state == capitalize_category - -@pytest.mark.parametrize("category, datetime_str, latest_value, country_name, \ - country_code, province, latest_country_value, \ - coordinate_lat, coordinate_long", - [("deaths", DATETIME_STRING, 1940, "Thailand", "TH", "", - 114, "15", "101"), - ("recovered", DATETIME_STRING, 1940, "Thailand", "TH", "", - 114, "15", "101"), - ("confirmed", DATETIME_STRING, 1940, "Thailand", "TH", "", - 114, "15", "101")]) -@mock.patch('app.services.location.jhu.datetime') -@mock.patch('app.services.location.jhu.requests.get', side_effect=mocked_requests_get) -def test_get_category(mock_request_get, mock_datetime, category, datetime_str, - latest_value, country_name, country_code, province, latest_country_value, - coordinate_lat, coordinate_long): - #mock app.services.location.jhu.datetime.utcnow().isoformat() - mock_datetime.utcnow.return_value.isoformat.return_value = datetime_str - output = jhu.get_category(category) - - #simple schema validation - assert output["source"] == "https://github.com/ExpDev07/coronavirus-tracker-api" - - assert isinstance(output["latest"], int) - assert output["latest"] == latest_value #based on example data - - #check for valid datestring - assert date.is_date(output["last_updated"]) is True - #ensure date formating - assert output["last_updated"] == datetime_str + "Z" #based on example data - - #validate location schema - location_entry = output["locations"][0] - - assert isinstance(location_entry["country"], str) - assert location_entry["country"] == country_name #based on example data - - assert isinstance(location_entry["country_code"], str) - assert len(location_entry["country_code"]) == 2 - assert location_entry["country_code"] == country_code #based on example data - - assert isinstance(location_entry["province"], str) - assert location_entry["province"] == province #based on example data - - assert isinstance(location_entry["latest"], int) - assert location_entry["latest"] == latest_country_value #based on example data - - #validate coordinates in location - coordinates = location_entry["coordinates"] - - assert isinstance(coordinates["lat"], str) - assert coordinates["lat"] == coordinate_lat - - assert isinstance(coordinates["long"], str) - assert coordinates["long"] == coordinate_long - - #validate history in location - history = location_entry["history"] - assert date.is_date(list(history.keys())[0]) is True - assert isinstance(list(history.values())[0], int) - @mock.patch('app.services.location.jhu.datetime') @mock.patch('app.services.location.jhu.requests.get', side_effect=mocked_requests_get) def test_get_locations(mock_request_get, mock_datetime): diff --git a/tests/test_routes.py b/tests/test_routes.py index 18138f09..e08eb349 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -86,7 +86,7 @@ def test_v2_latest(self, mock_request_get, mock_datetime): 'latest': { 'confirmed': 1940, 'deaths': 1940, - 'recovered': 1940 + 'recovered': 0 } }