From dfd1dc9459eebed7665894d0b07e0bced2175c4c Mon Sep 17 00:00:00 2001 From: ibhuiyan Date: Sat, 28 Mar 2020 16:23:56 -0400 Subject: [PATCH 01/38] moved v2 routes into v2 directory so it can work alongside v1 routes with FastAPI --- app/router/__init__.py | 2 +- app/router/{ => v2}/latest.py | 6 +++--- app/router/{ => v2}/locations.py | 8 ++++---- app/router/{ => v2}/sources.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename app/router/{ => v2}/latest.py (83%) rename app/router/{ => v2}/locations.py (91%) rename app/router/{ => v2}/sources.py (76%) diff --git a/app/router/__init__.py b/app/router/__init__.py index eefb5f0a..5827ead1 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -4,4 +4,4 @@ router = APIRouter() # The routes. -from . import latest, sources, locations # isort:skip +from .v2 import latest, sources, locations # isort:skip diff --git a/app/router/latest.py b/app/router/v2/latest.py similarity index 83% rename from app/router/latest.py rename to app/router/v2/latest.py index 81b254cf..cc250bb1 100644 --- a/app/router/latest.py +++ b/app/router/v2/latest.py @@ -1,8 +1,8 @@ from fastapi import Request -from ..enums.sources import Sources -from ..models.latest import LatestResponse as Latest -from . import router +from ...enums.sources import Sources +from ...models.latest import LatestResponse as Latest +from .. import router @router.get("/latest", response_model=Latest) diff --git a/app/router/locations.py b/app/router/v2/locations.py similarity index 91% rename from app/router/locations.py rename to app/router/v2/locations.py index af4b1cfd..acb0a9f4 100644 --- a/app/router/locations.py +++ b/app/router/v2/locations.py @@ -1,9 +1,9 @@ from fastapi import HTTPException, Request -from ..enums.sources import Sources -from ..models.location import LocationResponse as Location -from ..models.location import LocationsResponse as Locations -from . import router +from ...enums.sources import Sources +from ...models.location import LocationResponse as Location +from ...models.location import LocationsResponse as Locations +from .. import router @router.get("/locations", response_model=Locations, response_model_exclude_unset=True) diff --git a/app/router/sources.py b/app/router/v2/sources.py similarity index 76% rename from app/router/sources.py rename to app/router/v2/sources.py index 538921f4..d3807246 100644 --- a/app/router/sources.py +++ b/app/router/v2/sources.py @@ -1,5 +1,5 @@ -from ..data import data_sources -from . import router +from ...data import data_sources +from .. import router @router.get("/sources") From 4e6e8232380f87a6b0edfdbcd87fb5f6e387fa29 Mon Sep 17 00:00:00 2001 From: ibhuiyan Date: Sat, 28 Mar 2020 17:16:59 -0400 Subject: [PATCH 02/38] v2 passing tests with close to new proposed structure. Edited __init__ files so v1 and v2 have their own versions of 'router'. --- app/main.py | 4 +++- app/router/__init__.py | 3 ++- app/router/v2/__init__.py | 3 +++ app/router/v2/latest.py | 2 +- app/router/v2/locations.py | 2 +- app/router/v2/sources.py | 2 +- 6 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 app/router/v2/__init__.py diff --git a/app/main.py b/app/main.py index 44876182..0cb5bdf0 100644 --- a/app/main.py +++ b/app/main.py @@ -18,7 +18,7 @@ from .models.latest import LatestResponse as Latest from .models.location import LocationResponse as Location from .models.location import LocationsResponse as Locations -from .router import router +from .router.v2 import router # ############ # FastAPI App @@ -83,12 +83,14 @@ async def handle_validation_error(request: Request, exc: pydantic.error_wrappers # Include routers. +# APP.include_router(router, prefix="/", tags=["v1"]) APP.include_router(router, prefix="/v2", tags=["v2"]) # mount the existing Flask app # v1 @ / APP.mount("/", WSGIMiddleware(create_app())) + # Running of app. if __name__ == "__main__": uvicorn.run( diff --git a/app/router/__init__.py b/app/router/__init__.py index 5827ead1..98cb038d 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -1,7 +1,8 @@ from fastapi import APIRouter # Create the router. -router = APIRouter() +# router = APIRouter() # The routes. from .v2 import latest, sources, locations # isort:skip +# from .v1 import confirmed, deaths, recovered, all diff --git a/app/router/v2/__init__.py b/app/router/v2/__init__.py new file mode 100644 index 00000000..af9233c5 --- /dev/null +++ b/app/router/v2/__init__.py @@ -0,0 +1,3 @@ +from fastapi import APIRouter + +router = APIRouter() diff --git a/app/router/v2/latest.py b/app/router/v2/latest.py index cc250bb1..8e2e561b 100644 --- a/app/router/v2/latest.py +++ b/app/router/v2/latest.py @@ -2,7 +2,7 @@ from ...enums.sources import Sources from ...models.latest import LatestResponse as Latest -from .. import router +from . import router @router.get("/latest", response_model=Latest) diff --git a/app/router/v2/locations.py b/app/router/v2/locations.py index acb0a9f4..2fde5c9e 100644 --- a/app/router/v2/locations.py +++ b/app/router/v2/locations.py @@ -3,7 +3,7 @@ from ...enums.sources import Sources from ...models.location import LocationResponse as Location from ...models.location import LocationsResponse as Locations -from .. import router +from . import router @router.get("/locations", response_model=Locations, response_model_exclude_unset=True) diff --git a/app/router/v2/sources.py b/app/router/v2/sources.py index d3807246..4ade2fef 100644 --- a/app/router/v2/sources.py +++ b/app/router/v2/sources.py @@ -1,5 +1,5 @@ from ...data import data_sources -from .. import router +from . import router @router.get("/sources") From b6ec0c7a55b7f160b3810382372ecbb7a70b35e8 Mon Sep 17 00:00:00 2001 From: ibhuiyan Date: Sat, 28 Mar 2020 17:17:37 -0400 Subject: [PATCH 03/38] forgot to push main in last commit --- app/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index 0cb5bdf0..45ccc9a0 100644 --- a/app/main.py +++ b/app/main.py @@ -18,7 +18,7 @@ from .models.latest import LatestResponse as Latest from .models.location import LocationResponse as Location from .models.location import LocationsResponse as Locations -from .router.v2 import router +from .router.v2 import router as v2router # ############ # FastAPI App @@ -84,7 +84,7 @@ async def handle_validation_error(request: Request, exc: pydantic.error_wrappers # Include routers. # APP.include_router(router, prefix="/", tags=["v1"]) -APP.include_router(router, prefix="/v2", tags=["v2"]) +APP.include_router(v2router, prefix="/v2", tags=["v2"]) # mount the existing Flask app # v1 @ / From 1117c228001f3449398f81cdf9cde0e48f47b5d1 Mon Sep 17 00:00:00 2001 From: ibhuiyan Date: Sat, 28 Mar 2020 17:43:01 -0400 Subject: [PATCH 04/38] Replaced flask based v1. Not currently passing integration tests, need to change them to work with new implementation --- app/main.py | 7 +++++-- app/router/__init__.py | 5 +---- app/router/v1/__init__.py | 3 +++ app/router/v1/all.py | 21 +++++++++++++++++++++ app/router/v1/confirmed.py | 13 +++++++++++++ app/router/v1/deaths.py | 12 ++++++++++++ app/router/v1/recovered.py | 12 ++++++++++++ app/routes/__init__.py | 13 ------------- app/routes/v1/__init__.py | 0 app/routes/v1/all.py | 23 ----------------------- app/routes/v1/confirmed.py | 9 --------- app/routes/v1/deaths.py | 9 --------- app/routes/v1/recovered.py | 9 --------- 13 files changed, 67 insertions(+), 69 deletions(-) create mode 100644 app/router/v1/__init__.py create mode 100644 app/router/v1/all.py create mode 100644 app/router/v1/confirmed.py create mode 100644 app/router/v1/deaths.py create mode 100644 app/router/v1/recovered.py delete mode 100644 app/routes/__init__.py delete mode 100644 app/routes/v1/__init__.py delete mode 100644 app/routes/v1/all.py delete mode 100644 app/routes/v1/confirmed.py delete mode 100644 app/routes/v1/deaths.py delete mode 100644 app/routes/v1/recovered.py diff --git a/app/main.py b/app/main.py index 45ccc9a0..03899e73 100644 --- a/app/main.py +++ b/app/main.py @@ -10,7 +10,7 @@ import uvicorn from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware -from fastapi.middleware.wsgi import WSGIMiddleware +# from fastapi.middleware.wsgi import WSGIMiddleware from fastapi.responses import JSONResponse from .core import create_app @@ -18,6 +18,7 @@ from .models.latest import LatestResponse as Latest from .models.location import LocationResponse as Location from .models.location import LocationsResponse as Locations +from .router.v1 import router as v1router from .router.v2 import router as v2router # ############ @@ -83,12 +84,14 @@ async def handle_validation_error(request: Request, exc: pydantic.error_wrappers # Include routers. -# APP.include_router(router, prefix="/", tags=["v1"]) +APP.include_router(v1router, prefix="", tags=["v1"]) APP.include_router(v2router, prefix="/v2", tags=["v2"]) +''' # mount the existing Flask app # v1 @ / APP.mount("/", WSGIMiddleware(create_app())) +''' # Running of app. diff --git a/app/router/__init__.py b/app/router/__init__.py index 98cb038d..83dbead1 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -1,8 +1,5 @@ from fastapi import APIRouter -# Create the router. -# router = APIRouter() - # The routes. from .v2 import latest, sources, locations # isort:skip -# from .v1 import confirmed, deaths, recovered, all +from .v1 import confirmed, deaths, recovered, all diff --git a/app/router/v1/__init__.py b/app/router/v1/__init__.py new file mode 100644 index 00000000..af9233c5 --- /dev/null +++ b/app/router/v1/__init__.py @@ -0,0 +1,3 @@ +from fastapi import APIRouter + +router = APIRouter() diff --git a/app/router/v1/all.py b/app/router/v1/all.py new file mode 100644 index 00000000..dc51053a --- /dev/null +++ b/app/router/v1/all.py @@ -0,0 +1,21 @@ +#from flask import jsonify + +from ...services.location.jhu import get_category +from . import router + + +@router.get("/all") +def all(): + # Get all the categories. + confirmed = get_category("confirmed") + deaths = get_category("deaths") + recovered = get_category("recovered") + + return { + # Data. + "confirmed": confirmed, + "deaths": deaths, + "recovered": recovered, + # Latest. + "latest": {"confirmed":confirmed["latest"], "deaths": deaths["latest"], "recovered": recovered["latest"],}, + } diff --git a/app/router/v1/confirmed.py b/app/router/v1/confirmed.py new file mode 100644 index 00000000..b783afa5 --- /dev/null +++ b/app/router/v1/confirmed.py @@ -0,0 +1,13 @@ +# from flask import jsonify + +# from ...routes import api_v1 as api +from ...services.location.jhu import get_category +from . import router + + +@router.get("/confirmed") +def confirmed(): + + return { + get_category("confirmed") + } diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py new file mode 100644 index 00000000..a2710439 --- /dev/null +++ b/app/router/v1/deaths.py @@ -0,0 +1,12 @@ +# from flask import jsonify + +# from ...routes import api_v1 as api +from ...services.location.jhu import get_category +from . import router + + +@router.get("/deaths") +def deaths(): + return { + get_category("deaths") + } diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py new file mode 100644 index 00000000..2a034832 --- /dev/null +++ b/app/router/v1/recovered.py @@ -0,0 +1,12 @@ +# from flask import jsonify + +# from ...routes import api_v1 as api +from ...services.location.jhu import get_category +from . import router + + +@router.get("/recovered") +def recovered(): + return { + get_category("recovered") + } diff --git a/app/routes/__init__.py b/app/routes/__init__.py deleted file mode 100644 index 2a584490..00000000 --- a/app/routes/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -app.routes - -isort:skip_file -""" -from flask import Blueprint, redirect, request, abort, current_app as app -from ..data import data_source - -# Follow the import order to avoid circular dependency -api_v1 = Blueprint("api_v1", __name__, url_prefix="") - -# API version 1. -from .v1 import confirmed, deaths, recovered, all diff --git a/app/routes/v1/__init__.py b/app/routes/v1/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/routes/v1/all.py b/app/routes/v1/all.py deleted file mode 100644 index 9638c4bd..00000000 --- a/app/routes/v1/all.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask import jsonify - -from ...routes import api_v1 as api -from ...services.location.jhu import get_category - - -@api.route("/all") -def all(): - # Get all the categories. - confirmed = get_category("confirmed") - deaths = get_category("deaths") - recovered = get_category("recovered") - - return jsonify( - { - # Data. - "confirmed": confirmed, - "deaths": deaths, - "recovered": recovered, - # Latest. - "latest": {"confirmed": confirmed["latest"], "deaths": deaths["latest"], "recovered": recovered["latest"],}, - } - ) diff --git a/app/routes/v1/confirmed.py b/app/routes/v1/confirmed.py deleted file mode 100644 index 85cfe039..00000000 --- a/app/routes/v1/confirmed.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import jsonify - -from ...routes import api_v1 as api -from ...services.location.jhu import get_category - - -@api.route("/confirmed") -def confirmed(): - return jsonify(get_category("confirmed")) diff --git a/app/routes/v1/deaths.py b/app/routes/v1/deaths.py deleted file mode 100644 index cb65874b..00000000 --- a/app/routes/v1/deaths.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import jsonify - -from ...routes import api_v1 as api -from ...services.location.jhu import get_category - - -@api.route("/deaths") -def deaths(): - return jsonify(get_category("deaths")) diff --git a/app/routes/v1/recovered.py b/app/routes/v1/recovered.py deleted file mode 100644 index be5fe646..00000000 --- a/app/routes/v1/recovered.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import jsonify - -from ...routes import api_v1 as api -from ...services.location.jhu import get_category - - -@api.route("/recovered") -def recovered(): - return jsonify(get_category("recovered")) From 9502abe53ed590f1d026a581dafeb36c405152d6 Mon Sep 17 00:00:00 2001 From: ibhuiyan Date: Sat, 28 Mar 2020 20:19:44 -0400 Subject: [PATCH 05/38] Working v1 using FastAPI and passing tests. --- app/router/v1/confirmed.py | 8 ++++---- app/router/v1/deaths.py | 6 +++--- app/router/v1/recovered.py | 6 +++--- tests/test_routes.py | 26 +++++++++++++------------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/router/v1/confirmed.py b/app/router/v1/confirmed.py index b783afa5..d080374a 100644 --- a/app/router/v1/confirmed.py +++ b/app/router/v1/confirmed.py @@ -1,4 +1,5 @@ # from flask import jsonify +import json # from ...routes import api_v1 as api from ...services.location.jhu import get_category @@ -7,7 +8,6 @@ @router.get("/confirmed") def confirmed(): - - return { - get_category("confirmed") - } + confirmed = get_category("confirmed") + + return confirmed diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py index a2710439..b9be8954 100644 --- a/app/router/v1/deaths.py +++ b/app/router/v1/deaths.py @@ -7,6 +7,6 @@ @router.get("/deaths") def deaths(): - return { - get_category("deaths") - } + deaths = get_category("deaths") + + return deaths diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py index 2a034832..cfcf9983 100644 --- a/app/router/v1/recovered.py +++ b/app/router/v1/recovered.py @@ -7,6 +7,6 @@ @router.get("/recovered") def recovered(): - return { - get_category("recovered") - } + recovered = get_category("recovered") + + return recovered diff --git a/tests/test_routes.py b/tests/test_routes.py index 9e1c03ef..103f7505 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -6,8 +6,8 @@ import pytest from fastapi.testclient import TestClient -import app -from app import services +# import app +# from app import services from app.main import APP from .test_jhu import DATETIME_STRING, mocked_requests_get, mocked_strptime_isoformat @@ -23,10 +23,10 @@ class FlaskRoutesTest(unittest.TestCase): """ # load app context only once. - app = app.create_app() + # app = app.create_app() def setUp(self): - self.client = FlaskRoutesTest.app.test_client() + # self.client = FlaskRoutesTest.app.test_client() self.asgi_client = TestClient(APP) self.date = DATETIME_STRING @@ -48,36 +48,36 @@ def test_v1_confirmed(self, mock_request_get, mock_datetime): mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "confirmed" expected_json_output = self.read_file_v1(state=state) - return_data = self.client.get("/{}".format(state)).data.decode() + return_data = self.asgi_client.get("/{}".format(state)).json() - assert return_data == expected_json_output + assert return_data == json.loads(expected_json_output) def test_v1_deaths(self, mock_request_get, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "deaths" expected_json_output = self.read_file_v1(state=state) - return_data = self.client.get("/{}".format(state)).data.decode() + return_data = self.asgi_client.get("/{}".format(state)).json() - assert return_data == expected_json_output + assert return_data == json.loads(expected_json_output) def test_v1_recovered(self, mock_request_get, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "recovered" expected_json_output = self.read_file_v1(state=state) - return_data = self.client.get("/{}".format(state)).data.decode() + return_data = self.asgi_client.get("/{}".format(state)).json() - assert return_data == expected_json_output + assert return_data == json.loads(expected_json_output) def test_v1_all(self, mock_request_get, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "all" expected_json_output = self.read_file_v1(state=state) - return_data = self.client.get("/{}".format(state)).data.decode() - # print(return_data) - assert return_data == expected_json_output + return_data = self.asgi_client.get("/{}".format(state)).json() + + assert return_data == json.loads(expected_json_output) def test_v2_latest(self, mock_request_get, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING From 7b22966931edc0fd04cc776db2728582be7ea59a Mon Sep 17 00:00:00 2001 From: ibhuiyan Date: Sat, 28 Mar 2020 20:41:56 -0400 Subject: [PATCH 06/38] Cleaned up changes for transfer of v1 from mounted as a wsgi application fo FastAPI --- app/main.py | 7 ------- app/router/v1/all.py | 2 -- app/router/v1/confirmed.py | 4 ---- app/router/v1/deaths.py | 3 --- app/router/v1/recovered.py | 3 --- tests/test_routes.py | 4 ---- 6 files changed, 23 deletions(-) diff --git a/app/main.py b/app/main.py index 03899e73..f977622d 100644 --- a/app/main.py +++ b/app/main.py @@ -10,7 +10,6 @@ import uvicorn from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware -# from fastapi.middleware.wsgi import WSGIMiddleware from fastapi.responses import JSONResponse from .core import create_app @@ -87,12 +86,6 @@ async def handle_validation_error(request: Request, exc: pydantic.error_wrappers APP.include_router(v1router, prefix="", tags=["v1"]) APP.include_router(v2router, prefix="/v2", tags=["v2"]) -''' -# mount the existing Flask app -# v1 @ / -APP.mount("/", WSGIMiddleware(create_app())) -''' - # Running of app. if __name__ == "__main__": diff --git a/app/router/v1/all.py b/app/router/v1/all.py index dc51053a..bf0f7a64 100644 --- a/app/router/v1/all.py +++ b/app/router/v1/all.py @@ -1,5 +1,3 @@ -#from flask import jsonify - from ...services.location.jhu import get_category from . import router diff --git a/app/router/v1/confirmed.py b/app/router/v1/confirmed.py index d080374a..0a8ab1c3 100644 --- a/app/router/v1/confirmed.py +++ b/app/router/v1/confirmed.py @@ -1,7 +1,3 @@ -# from flask import jsonify -import json - -# from ...routes import api_v1 as api from ...services.location.jhu import get_category from . import router diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py index b9be8954..740e4101 100644 --- a/app/router/v1/deaths.py +++ b/app/router/v1/deaths.py @@ -1,6 +1,3 @@ -# from flask import jsonify - -# from ...routes import api_v1 as api from ...services.location.jhu import get_category from . import router diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py index cfcf9983..bfb21a51 100644 --- a/app/router/v1/recovered.py +++ b/app/router/v1/recovered.py @@ -1,6 +1,3 @@ -# from flask import jsonify - -# from ...routes import api_v1 as api from ...services.location.jhu import get_category from . import router diff --git a/tests/test_routes.py b/tests/test_routes.py index 103f7505..b1448f9a 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -22,11 +22,7 @@ class FlaskRoutesTest(unittest.TestCase): Store all integration testcases in one class to ensure app context """ - # load app context only once. - # app = app.create_app() - def setUp(self): - # self.client = FlaskRoutesTest.app.test_client() self.asgi_client = TestClient(APP) self.date = DATETIME_STRING From 6638e6e76b9e532a6a1340f0e3165a1ff01cad02 Mon Sep 17 00:00:00 2001 From: ibhuiyan Date: Sat, 28 Mar 2020 21:13:09 -0400 Subject: [PATCH 07/38] After running formatters to fix build issue --- app/router/__init__.py | 3 ++- app/router/v1/all.py | 2 +- app/router/v1/deaths.py | 2 +- app/router/v1/recovered.py | 2 +- tests/test_routes.py | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/router/__init__.py b/app/router/__init__.py index 83dbead1..e37fdd9f 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -1,5 +1,6 @@ from fastapi import APIRouter +from .v1 import all, confirmed, deaths, recovered + # The routes. from .v2 import latest, sources, locations # isort:skip -from .v1 import confirmed, deaths, recovered, all diff --git a/app/router/v1/all.py b/app/router/v1/all.py index bf0f7a64..e528ed5a 100644 --- a/app/router/v1/all.py +++ b/app/router/v1/all.py @@ -15,5 +15,5 @@ def all(): "deaths": deaths, "recovered": recovered, # Latest. - "latest": {"confirmed":confirmed["latest"], "deaths": deaths["latest"], "recovered": recovered["latest"],}, + "latest": {"confirmed": confirmed["latest"], "deaths": deaths["latest"], "recovered": recovered["latest"],}, } diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py index 740e4101..b3d90413 100644 --- a/app/router/v1/deaths.py +++ b/app/router/v1/deaths.py @@ -5,5 +5,5 @@ @router.get("/deaths") def deaths(): deaths = get_category("deaths") - + return deaths diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py index bfb21a51..e9ae8f72 100644 --- a/app/router/v1/recovered.py +++ b/app/router/v1/recovered.py @@ -5,5 +5,5 @@ @router.get("/recovered") def recovered(): recovered = get_category("recovered") - + return recovered diff --git a/tests/test_routes.py b/tests/test_routes.py index b1448f9a..48d804e5 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -72,7 +72,7 @@ def test_v1_all(self, mock_request_get, mock_datetime): state = "all" expected_json_output = self.read_file_v1(state=state) return_data = self.asgi_client.get("/{}".format(state)).json() - + assert return_data == json.loads(expected_json_output) def test_v2_latest(self, mock_request_get, mock_datetime): From 7ecc9da4fdfa9449093a8f017ccf2cc11a908f33 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2020 01:33:00 +0000 Subject: [PATCH 08/38] docs: update README.md [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 87be1edd..7703aa2d 100644 --- a/README.md +++ b/README.md @@ -474,6 +474,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Turreted

💻 +
Ibtida Bhuiyan

💻 From 9194f9a1e95fee64da64a7f39a781fa2cee49286 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2020 01:33:01 +0000 Subject: [PATCH 09/38] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0f568cbd..bbf06353 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -153,6 +153,15 @@ "contributions": [ "code" ] + }, + { + "login": "ibhuiyan17", + "name": "Ibtida Bhuiyan", + "avatar_url": "https://avatars1.githubusercontent.com/u/33792969?v=4", + "profile": "http://ibtida.me", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, From 06ae920364c515abc3bbdf9803e984543a24d23d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 29 Mar 2020 17:09:02 -0400 Subject: [PATCH 10/38] Move to invoke task runner (#222) * add invoke as dev dependency * invoke tasks * add invoke check cmd update pipenv scripts and make commands * optional --diff * lint and test * update docs remove Flask as a project requirement add invoke commands * add lint and test to Pipenv scripts, update docs --- Makefile | 6 ++--- Pipfile | 7 ++++-- Pipfile.lock | 11 ++++++++- README.md | 19 +++++---------- tasks.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 tasks.py diff --git a/Makefile b/Makefile index 78f8f6b4..7059a9be 100644 --- a/Makefile +++ b/Makefile @@ -21,9 +21,7 @@ lint: pylint $(APP) || true fmt: - isort --apply --atomic - black . -l 120 + invoke fmt check-fmt: - isort -rc --check - black . --check --diff + invoke check --fmt --sort diff --git a/Pipfile b/Pipfile index 6b30ef90..21965c37 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ verify_ssl = true [dev-packages] bandit = "*" black = "==19.10b0" +invoke = "*" isort = "*" pytest = "*" pylint = "*" @@ -27,5 +28,7 @@ python_version = "3.8" [scripts] dev = "uvicorn app.main:APP --reload" start = "uvicorn app.main:APP" -fmt = "black . -l 120" -sort = "isort --apply --atomic" +fmt = "invoke fmt" +sort = "invoke sort" +lint = "invoke lint" +test = "invoke test" diff --git a/Pipfile.lock b/Pipfile.lock index c949f6cb..cc93e092 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ecd83aad2c3783fdaa5581f562d022a6b500b3f3b4beb7c3f63d3d5baff85813" + "sha256": "9574394caac3b437d4357507a93178b38289861241165e4736369a7b4c2a2cbc" }, "pipfile-spec": 6, "requires": { @@ -342,6 +342,15 @@ ], "version": "==3.1.0" }, + "invoke": { + "hashes": [ + "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132", + "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134", + "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d" + ], + "index": "pypi", + "version": "==1.4.1" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", diff --git a/README.md b/README.md index 7703aa2d..219d8cc4 100644 --- a/README.md +++ b/README.md @@ -391,7 +391,6 @@ These are the available API wrappers created by the community. They are not nece You will need the following things properly installed on your computer. * [Python 3](https://www.python.org/downloads/) (with pip) -* [Flask](https://pypi.org/project/Flask/) * [pipenv](https://pypi.org/project/pipenv/) ## Installation @@ -415,32 +414,26 @@ And don't despair if don't get the python setup working on the first try. No one * Visit your app at [http://localhost:5000](http://localhost:5000). ### Running Tests +> [pytest](https://docs.pytest.org/en/latest/) ```bash -pipenv sync --dev -pipenv shell -make test +pipenv run test ``` + ### Linting +> [pylint](https://www.pylint.org/) ```bash -pipenv sync --dev -pipenv shell -make lint +pipenv run lint ``` ### Formatting +> [black](https://black.readthedocs.io/en/stable/) ```bash pipenv run fmt ``` -or -```bash -pipenv shell -make fmt -``` - ### Building diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..3ff5f24c --- /dev/null +++ b/tasks.py @@ -0,0 +1,66 @@ +""" +tasks.py +-------- +Project invoke tasks + +Available commands + invoke --list + invoke fmt + invoke sort + invoke check +""" +import invoke + +TARGETS_DESCRIPTION = "Paths/directories to format. [default: . ]" + + +@invoke.task(help={"targets": TARGETS_DESCRIPTION}) +def sort(ctx, targets="."): + """Sort module imports.""" + print("sorting imports ...") + args = ["isort", "-rc", "--atomic", targets] + ctx.run(" ".join(args)) + + +@invoke.task(pre=[sort], help={"targets": TARGETS_DESCRIPTION}) +def fmt(ctx, targets="."): + """Format python source code & sort imports.""" + print("formatting ...") + args = ["black", targets] + ctx.run(" ".join(args)) + + +@invoke.task +def check(ctx, fmt=False, sort=False, diff=False): # pylint: disable=redefined-outer-name + """Check code format and import order.""" + if not any([fmt, sort]): + fmt = True + sort = True + + fmt_args = ["black", "--check", "."] + sort_args = ["isort", "-rc", "--check", "."] + + if diff: + fmt_args.append("--diff") + sort_args.append("--diff") + + cmd_args = [] + if fmt: + cmd_args.extend(fmt_args) + if sort: + if cmd_args: + cmd_args.append("&") + cmd_args.extend(sort_args) + ctx.run(" ".join(cmd_args)) + + +@invoke.task +def lint(ctx): + """Run linter.""" + ctx.run(" ".join(["pylint", "app"])) + + +@invoke.task +def test(ctx): + """Run pytest tests.""" + ctx.run(" ".join(["pytest", "-v"])) From 2080bf835d4c4e63a957fe07b180bacae299a15e Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 30 Mar 2020 18:24:20 -0400 Subject: [PATCH 11/38] Update dependencies, coverage with pytest-cov (#239) * remove flask * sort dependencies * add coveralls badge * add coveralls * update make test to create coverage reports * split script commands, add coveralls upload step * remove unneeded modules and imports * regenerate lockfile --- .travis.yml | 6 +- Makefile | 3 +- Pipfile | 12 ++-- Pipfile.lock | 177 +++++++++++++++++++++++++++--------------------- README.md | 1 + app/__init__.py | 2 - app/core.py | 24 ------- app/main.py | 6 -- 8 files changed, 111 insertions(+), 120 deletions(-) delete mode 100644 app/core.py diff --git a/.travis.yml b/.travis.yml index 39e16fce..3ea52aa9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,8 @@ install: - "pip install pipenv" - "pipenv install --dev --skip-lock" script: - - "make test lint check-fmt" + - "make test" + - "make lint" + - "make check-fmt" +after_success: + - coveralls diff --git a/Makefile b/Makefile index 7059a9be..10c88fb0 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,7 @@ APP = app TEST = tests test: - $(PYTHON) `which py.test` -s -v $(TEST) - + pytest -v $(TEST) --cov-report term --cov-report xml --cov=$(APP) lint: pylint $(APP) || true diff --git a/Pipfile b/Pipfile index 21965c37..2e5b9a1f 100644 --- a/Pipfile +++ b/Pipfile @@ -6,20 +6,20 @@ verify_ssl = true [dev-packages] bandit = "*" black = "==19.10b0" +coveralls = "*" invoke = "*" isort = "*" -pytest = "*" pylint = "*" +pytest = "*" +pytest-cov = "*" [packages] +cachetools = "*" fastapi = "*" -flask = "*" -python-dotenv = "*" -requests = "*" gunicorn = "*" -flask-cors = "*" -cachetools = "*" python-dateutil = "*" +python-dotenv = "*" +requests = "*" uvicorn = "*" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index cc93e092..6d4da039 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9574394caac3b437d4357507a93178b38289861241165e4736369a7b4c2a2cbc" + "sha256": "6135cc5a7f50377967629ba129a9747170d933706e42b8b74e29a5d4713aa4e0" }, "pipfile-spec": 6, "requires": { @@ -47,27 +47,11 @@ }, "fastapi": { "hashes": [ - "sha256:532648b4e16dd33673d71dc0b35dff1b4d20c709d04078010e258b9f3a79771a", - "sha256:721b11d8ffde52c669f52741b6d9d761fe2e98778586f4cfd6f5e47254ba5016" + "sha256:c2d572370153a6b74d62a73252d75934e2bfdbb0f620fecfd489b5d4789f5c48", + "sha256:c478bc513d192f6776fd3f0355b7ff5414e94ed842677294c06e348105aaa237" ], "index": "pypi", - "version": "==0.52.0" - }, - "flask": { - "hashes": [ - "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", - "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" - ], - "index": "pypi", - "version": "==1.1.1" - }, - "flask-cors": { - "hashes": [ - "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", - "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a" - ], - "index": "pypi", - "version": "==3.0.8" + "version": "==0.53.1" }, "gunicorn": { "hashes": [ @@ -109,58 +93,6 @@ ], "version": "==2.9" }, - "itsdangerous": { - "hashes": [ - "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", - "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" - ], - "version": "==1.1.0" - }, - "jinja2": { - "hashes": [ - "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", - "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" - ], - "version": "==2.11.1" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" - ], - "version": "==1.1.1" - }, "pydantic": { "hashes": [ "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752", @@ -274,13 +206,6 @@ "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" ], "version": "==8.1" - }, - "werkzeug": { - "hashes": [ - "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096", - "sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16" - ], - "version": "==1.0.0" } }, "develop": { @@ -321,6 +246,20 @@ "index": "pypi", "version": "==19.10b0" }, + "certifi": { + "hashes": [ + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + ], + "version": "==2019.11.28" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "click": { "hashes": [ "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", @@ -328,6 +267,56 @@ ], "version": "==7.1.1" }, + "coverage": { + "hashes": [ + "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0", + "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30", + "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b", + "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0", + "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823", + "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe", + "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037", + "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6", + "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31", + "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd", + "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892", + "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1", + "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78", + "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac", + "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006", + "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014", + "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2", + "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7", + "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8", + "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7", + "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9", + "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1", + "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307", + "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a", + "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435", + "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0", + "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5", + "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441", + "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732", + "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de", + "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1" + ], + "version": "==5.0.4" + }, + "coveralls": { + "hashes": [ + "sha256:4b6bfc2a2a77b890f556bc631e35ba1ac21193c356393b66c84465c06218e135", + "sha256:67188c7ec630c5f708c31552f2bcdac4580e172219897c4136504f14b823132f" + ], + "index": "pypi", + "version": "==1.11.1" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, "gitdb": { "hashes": [ "sha256:284a6a4554f954d6e737cddcff946404393e030b76a282c6640df8efd6b3da5e", @@ -342,6 +331,13 @@ ], "version": "==3.1.0" }, + "idna": { + "hashes": [ + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + ], + "version": "==2.9" + }, "invoke": { "hashes": [ "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132", @@ -457,6 +453,14 @@ "index": "pypi", "version": "==5.4.1" }, + "pytest-cov": { + "hashes": [ + "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", + "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" + ], + "index": "pypi", + "version": "==2.8.1" + }, "pyyaml": { "hashes": [ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", @@ -499,6 +503,14 @@ ], "version": "==2020.2.20" }, + "requests": { + "hashes": [ + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + ], + "index": "pypi", + "version": "==2.23.0" + }, "six": { "hashes": [ "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", @@ -553,6 +565,13 @@ ], "version": "==1.4.1" }, + "urllib3": { + "hashes": [ + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + ], + "version": "==1.25.8" + }, "wcwidth": { "hashes": [ "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", diff --git a/README.md b/README.md index 219d8cc4..df66078d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Provides up-to-date data about Coronavirus outbreak. Includes numbers about conf Support multiple data-sources. ![Travis build](https://api.travis-ci.com/ExpDev07/coronavirus-tracker-api.svg?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/ExpDev07/coronavirus-tracker-api/badge.svg?branch=master)](https://coveralls.io/github/ExpDev07/coronavirus-tracker-api?branch=master) [![License](https://img.shields.io/github/license/ExpDev07/coronavirus-tracker-api)](LICENSE.md) [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) [![GitHub stars](https://img.shields.io/github/stars/ExpDev07/coronavirus-tracker-api)](https://github.com/ExpDev07/coronavirus-tracker-api/stargazers) diff --git a/app/__init__.py b/app/__init__.py index 16847abb..76345caa 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,2 @@ # See PEP396. __version__ = "2.0" - -from .core import create_app diff --git a/app/core.py b/app/core.py deleted file mode 100644 index ef22b686..00000000 --- a/app/core.py +++ /dev/null @@ -1,24 +0,0 @@ -from flask import Flask -from flask_cors import CORS - - -def create_app(): - """ - Construct the core application. - """ - # Create flask app with CORS enabled. - app = Flask(__name__) - CORS(app) - - # Set app config from settings. - app.config.from_pyfile("config/settings.py") - - with app.app_context(): - # Import routes. - from . import routes - - # Register api endpoints. - app.register_blueprint(routes.api_v1) - - # Return created app. - return app diff --git a/app/main.py b/app/main.py index f977622d..b34d6629 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,8 @@ """ app.main.py """ -import datetime as dt import logging import os -import reprlib import pydantic import uvicorn @@ -12,11 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -from .core import create_app from .data import data_source -from .models.latest import LatestResponse as Latest -from .models.location import LocationResponse as Location -from .models.location import LocationsResponse as Locations from .router.v1 import router as v1router from .router.v2 import router as v2router From 8129824f4819a35ac5b0ab30ea2f723d107bb7b5 Mon Sep 17 00:00:00 2001 From: GRIBOK <40306040+gribok@users.noreply.github.com> Date: Thu, 2 Apr 2020 00:25:56 +0200 Subject: [PATCH 12/38] add contrib guide. See ExpDev07/coronavirus-tracker-api#169 (#243) --- CONTRIBUTING.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..1fde2f33 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contribution to Coronavirus Tracker API + +First off, thanks for taking the time to contribute! +Every commit supports the open source ecosystem in case of [COVID-19](https://en.wikipedia.org/wiki/2019%E2%80%9320_coronavirus_pandemic). + +## Testing + +We have a handful of unit tests to cover most of functions. +Please write new test cases for new code you create. + +## Submitting changes + +* If you're unable to find an open issue, [open a new one](https://github.com/ExpDev07/coronavirus-tracker-api/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible +* Open a new [GitHub Pull Request to coronavirus-tracker-api](https://github.com/ExpDev07/coronavirus-tracker-api/pulls) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). Include the relevant issue number if applicable. +* We will love you forever if you include unit tests. We can always use more test coverage + +## Your First Code Contribution + +Unsure where to begin contributing to coronavirus-tracker-api ? You can start by looking through these issues labels: + +* [Enhancement issues](https://github.com/ExpDev07/coronavirus-tracker-api/labels/enhancement) - issues for new feature or request +* [Help wanted issues](https://github.com/ExpDev07/coronavirus-tracker-api/labels/help%20wanted) - extra attention is needed +* [Documentation issues](https://github.com/ExpDev07/coronavirus-tracker-api/labels/documentation) - improvements or additions to documentation + +## Styleguide + +Please follow [PEP8](https://www.python.org/dev/peps/pep-0008/) guide. +See [Running Test](./README.md#running-tests), [Linting](./README.md#linting) and [Formatting](./README.md#formatting) sections for further instructions to validate your change. + + +We encourage you to pitch in and join the [Coronavirus Tracker API Team](https://github.com/ExpDev07/coronavirus-tracker-api#contributors-)! + +Thanks! :heart: :heart: :heart: + +[Coronavirus Tracker API Team](https://github.com/ExpDev07/coronavirus-tracker-api#contributors-) From 2f95275147fdd678569d02d323b2b61d003b9d6b Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 2 Apr 2020 07:10:19 -0400 Subject: [PATCH 13/38] Address Linting warnings (#254) * lint fixes * re-enable CI failure on lint warnings * edit lint config * white list pydantic * allow fstring logging * allow TODO * temporarily allow duplicate code --- Makefile | 2 +- app/__init__.py | 7 ++++++- app/config/settings.py | 7 +++---- app/coordinates.py | 3 +++ app/data/__init__.py | 5 +++-- app/location/__init__.py | 12 +++++++++--- app/location/csbs.py | 4 +++- app/main.py | 21 +++++++++++++-------- app/router/__init__.py | 2 ++ app/router/v1/__init__.py | 3 ++- app/router/v1/all.py | 9 +++++---- app/router/v1/confirmed.py | 10 +++++----- app/router/v1/deaths.py | 10 +++++----- app/router/v1/recovered.py | 10 +++++----- app/router/v2/__init__.py | 3 ++- app/router/v2/latest.py | 7 ++++--- app/router/v2/locations.py | 9 ++++++--- app/router/v2/sources.py | 9 +++++---- app/services/location/__init__.py | 3 ++- app/services/location/csbs.py | 13 +++++++------ app/services/location/jhu.py | 13 ++++++------- app/timeline.py | 8 ++++---- app/utils/countries.py | 16 ++++++++-------- app/utils/date.py | 1 + app/utils/populations.py | 15 ++++++--------- pylintrc | 10 +++++----- tests/test_countries.py | 4 ++-- 27 files changed, 123 insertions(+), 93 deletions(-) diff --git a/Makefile b/Makefile index 10c88fb0..311b6bc4 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ TEST = tests test: pytest -v $(TEST) --cov-report term --cov-report xml --cov=$(APP) lint: - pylint $(APP) || true + pylint $(APP) fmt: invoke fmt diff --git a/app/__init__.py b/app/__init__.py index 76345caa..c43ae7ac 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,2 +1,7 @@ +""" +Corona Virus Tracker API +~~~~~~~~~~~~~~~~~~~~~~~~ +API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak. +""" # See PEP396. -__version__ = "2.0" +__version__ = "2.0.1" diff --git a/app/config/settings.py b/app/config/settings.py index 27c907bd..4a02a734 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -1,3 +1,4 @@ +"""app.config.settings.py""" import os # Load enviroment variables from .env file. @@ -5,7 +6,5 @@ load_dotenv() -""" -The port to serve the app application on. -""" -PORT = int(os.getenv("PORT", 5000)) +# The port to serve the app application on. +PORT = int(os.getenv("PORT", "5000")) diff --git a/app/coordinates.py b/app/coordinates.py index cc27a8e3..be972c6e 100644 --- a/app/coordinates.py +++ b/app/coordinates.py @@ -1,3 +1,6 @@ +"""app.coordinates.py""" + + class Coordinates: """ A position on earth using decimal coordinates (latitude and longitude). diff --git a/app/data/__init__.py b/app/data/__init__.py index 73468add..aef58e8c 100644 --- a/app/data/__init__.py +++ b/app/data/__init__.py @@ -1,8 +1,9 @@ +"""app.data""" from ..services.location.csbs import CSBSLocationService from ..services.location.jhu import JhuLocationService # Mapping of services to data-sources. -data_sources = {"jhu": JhuLocationService(), "csbs": CSBSLocationService()} +DATA_SOURCES = {"jhu": JhuLocationService(), "csbs": CSBSLocationService()} def data_source(source): @@ -12,4 +13,4 @@ def data_source(source): :returns: The service. :rtype: LocationService """ - return data_sources.get(source.lower()) + return DATA_SOURCES.get(source.lower()) diff --git a/app/location/__init__.py b/app/location/__init__.py index 4782fddb..d12f28c3 100644 --- a/app/location/__init__.py +++ b/app/location/__init__.py @@ -1,14 +1,18 @@ +"""app.location""" from ..coordinates import Coordinates from ..utils import countries from ..utils.populations import country_population -class Location: +# pylint: disable=redefined-builtin,invalid-name +class Location: # pylint: disable=too-many-instance-attributes """ A location in the world affected by the coronavirus. """ - def __init__(self, id, country, province, coordinates, last_updated, confirmed, deaths, recovered): + def __init__( + self, id, country, province, coordinates, last_updated, confirmed, deaths, recovered + ): # pylint: disable=too-many-arguments # General info. self.id = id self.country = country.strip() @@ -31,7 +35,7 @@ def country_code(self): :returns: The country code. :rtype: str """ - return (countries.country_code(self.country) or countries.default_country_code).upper() + return (countries.country_code(self.country) or countries.DEFAULT_COUNTRY_CODE).upper() @property def country_population(self): @@ -71,6 +75,7 @@ class TimelinedLocation(Location): A location with timelines. """ + # pylint: disable=too-many-arguments def __init__(self, id, country, province, coordinates, last_updated, timelines): super().__init__( # General info. @@ -88,6 +93,7 @@ def __init__(self, id, country, province, coordinates, last_updated, timelines): # Set timelines. self.timelines = timelines + # pylint: disable=arguments-differ def serialize(self, timelines=False): """ Serializes the location into a dict. diff --git a/app/location/csbs.py b/app/location/csbs.py index 0b7c27f8..649e8b22 100644 --- a/app/location/csbs.py +++ b/app/location/csbs.py @@ -1,3 +1,4 @@ +"""app.locations.csbs.py""" from . import Location @@ -6,6 +7,7 @@ class CSBSLocation(Location): A CSBS (county) location. """ + # pylint: disable=too-many-arguments,redefined-builtin def __init__(self, id, state, county, coordinates, last_updated, confirmed, deaths): super().__init__( # General info. @@ -23,7 +25,7 @@ def __init__(self, id, state, county, coordinates, last_updated, confirmed, deat self.state = state self.county = county - def serialize(self, timelines=False): + def serialize(self, timelines=False): # pylint: disable=arguments-differ,unused-argument """ Serializes the location into a dict. diff --git a/app/main.py b/app/main.py index b34d6629..75805ebc 100644 --- a/app/main.py +++ b/app/main.py @@ -11,8 +11,8 @@ from fastapi.responses import JSONResponse from .data import data_source -from .router.v1 import router as v1router -from .router.v2 import router as v2router +from .router.v1 import V1 +from .router.v2 import V2 # ############ # FastAPI App @@ -21,7 +21,10 @@ APP = FastAPI( title="Coronavirus Tracker", - description="API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak. Project page: https://github.com/ExpDev07/coronavirus-tracker-api.", + description=( + "API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak." + " Project page: https://github.com/ExpDev07/coronavirus-tracker-api." + ), version="2.0.1", docs_url="/", redoc_url="/docs", @@ -36,7 +39,7 @@ CORSMiddleware, allow_credentials=True, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) -# TODO this could probably just be a FastAPI dependency. + @APP.middleware("http") async def add_datasource(request: Request, call_next): """ @@ -64,7 +67,9 @@ async def add_datasource(request: Request, call_next): @APP.exception_handler(pydantic.error_wrappers.ValidationError) -async def handle_validation_error(request: Request, exc: pydantic.error_wrappers.ValidationError): +async def handle_validation_error( + request: Request, exc: pydantic.error_wrappers.ValidationError +): # pylint: disable=unused-argument """ Handles validation errors. """ @@ -77,12 +82,12 @@ async def handle_validation_error(request: Request, exc: pydantic.error_wrappers # Include routers. -APP.include_router(v1router, prefix="", tags=["v1"]) -APP.include_router(v2router, prefix="/v2", tags=["v2"]) +APP.include_router(V1, prefix="", tags=["v1"]) +APP.include_router(V2, prefix="/v2", tags=["v2"]) # Running of app. if __name__ == "__main__": uvicorn.run( - "app.main:APP", host="127.0.0.1", port=int(os.getenv("PORT", 5000)), log_level="info", + "app.main:APP", host="127.0.0.1", port=int(os.getenv("PORT", "5000")), log_level="info", ) diff --git a/app/router/__init__.py b/app/router/__init__.py index e37fdd9f..4eda6c21 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -1,5 +1,7 @@ +"""app.router""" from fastapi import APIRouter +# pylint: disable=redefined-builtin from .v1 import all, confirmed, deaths, recovered # The routes. diff --git a/app/router/v1/__init__.py b/app/router/v1/__init__.py index af9233c5..839bd212 100644 --- a/app/router/v1/__init__.py +++ b/app/router/v1/__init__.py @@ -1,3 +1,4 @@ +"""app.router.v1""" from fastapi import APIRouter -router = APIRouter() +V1 = APIRouter() diff --git a/app/router/v1/all.py b/app/router/v1/all.py index e528ed5a..b26fe25b 100644 --- a/app/router/v1/all.py +++ b/app/router/v1/all.py @@ -1,10 +1,11 @@ +"""app.router.v1.all.py""" from ...services.location.jhu import get_category -from . import router +from . import V1 -@router.get("/all") -def all(): - # Get all the categories. +@V1.get("/all") +def all(): # pylint: disable=redefined-builtin + """Get all the categories.""" confirmed = get_category("confirmed") deaths = get_category("deaths") recovered = get_category("recovered") diff --git a/app/router/v1/confirmed.py b/app/router/v1/confirmed.py index 0a8ab1c3..f3b97523 100644 --- a/app/router/v1/confirmed.py +++ b/app/router/v1/confirmed.py @@ -1,9 +1,9 @@ +"""app.router.v1.confirmed.py""" from ...services.location.jhu import get_category -from . import router +from . import V1 -@router.get("/confirmed") +@V1.get("/confirmed") def confirmed(): - confirmed = get_category("confirmed") - - return confirmed + """Confirmed cases.""" + return get_category("confirmed") diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py index b3d90413..65ed0967 100644 --- a/app/router/v1/deaths.py +++ b/app/router/v1/deaths.py @@ -1,9 +1,9 @@ +"""app.router.v1.deaths.py""" from ...services.location.jhu import get_category -from . import router +from . import V1 -@router.get("/deaths") +@V1.get("/deaths") def deaths(): - deaths = get_category("deaths") - - return deaths + """Total deaths.""" + return get_category("deaths") diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py index e9ae8f72..254823ed 100644 --- a/app/router/v1/recovered.py +++ b/app/router/v1/recovered.py @@ -1,9 +1,9 @@ +"""app.router.v1.recovered.py""" from ...services.location.jhu import get_category -from . import router +from . import V1 -@router.get("/recovered") +@V1.get("/recovered") def recovered(): - recovered = get_category("recovered") - - return recovered + """Recovered cases.""" + return get_category("recovered") diff --git a/app/router/v2/__init__.py b/app/router/v2/__init__.py index af9233c5..62c31905 100644 --- a/app/router/v2/__init__.py +++ b/app/router/v2/__init__.py @@ -1,3 +1,4 @@ +"""app.router.v2""" from fastapi import APIRouter -router = APIRouter() +V2 = APIRouter() diff --git a/app/router/v2/latest.py b/app/router/v2/latest.py index 8e2e561b..071c3a22 100644 --- a/app/router/v2/latest.py +++ b/app/router/v2/latest.py @@ -1,12 +1,13 @@ +"""app.router.v2.latest.py""" from fastapi import Request from ...enums.sources import Sources from ...models.latest import LatestResponse as Latest -from . import router +from . import V2 -@router.get("/latest", response_model=Latest) -def get_latest(request: Request, source: Sources = "jhu"): +@V2.get("/latest", response_model=Latest) +def get_latest(request: Request, source: Sources = "jhu"): # pylint: disable=unused-argument """ Getting latest amount of total confirmed cases, deaths, and recoveries. """ diff --git a/app/router/v2/locations.py b/app/router/v2/locations.py index 2fde5c9e..815b1eb8 100644 --- a/app/router/v2/locations.py +++ b/app/router/v2/locations.py @@ -1,12 +1,14 @@ +"""app.router.v2.locations.py""" from fastapi import HTTPException, Request from ...enums.sources import Sources from ...models.location import LocationResponse as Location from ...models.location import LocationsResponse as Locations -from . import router +from . import V2 -@router.get("/locations", response_model=Locations, response_model_exclude_unset=True) +# pylint: disable=unused-argument,too-many-arguments,redefined-builtin +@V2.get("/locations", response_model=Locations, response_model_exclude_unset=True) def get_locations( request: Request, source: Sources = "jhu", @@ -53,7 +55,8 @@ def get_locations( } -@router.get("/locations/{id}", response_model=Location) +# pylint: disable=invalid-name +@V2.get("/locations/{id}", response_model=Location) def get_location_by_id(request: Request, id: int, source: Sources = "jhu", timelines: bool = True): """ Getting specific location by id. diff --git a/app/router/v2/sources.py b/app/router/v2/sources.py index 4ade2fef..ad906e51 100644 --- a/app/router/v2/sources.py +++ b/app/router/v2/sources.py @@ -1,10 +1,11 @@ -from ...data import data_sources -from . import router +"""app.router.v2.sources.py""" +from ...data import DATA_SOURCES +from . import V2 -@router.get("/sources") +@V2.get("/sources") async def sources(): """ Retrieves a list of data-sources that are availble to use. """ - return {"sources": list(data_sources.keys())} + return {"sources": list(DATA_SOURCES.keys())} diff --git a/app/services/location/__init__.py b/app/services/location/__init__.py index 80b5e05c..404e9f7e 100644 --- a/app/services/location/__init__.py +++ b/app/services/location/__init__.py @@ -1,3 +1,4 @@ +"""app.services.location""" from abc import ABC, abstractmethod @@ -17,7 +18,7 @@ def get_all(self): raise NotImplementedError @abstractmethod - def get(self, id): + def get(self, id): # pylint: disable=redefined-builtin,invalid-name """ Gets and returns location with the provided id. diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index 6a13f41e..84654963 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -1,3 +1,4 @@ +"""app.services.location.csbs.py""" import csv from datetime import datetime @@ -18,12 +19,12 @@ def get_all(self): # Get the locations return get_locations() - def get(self, id): - return self.get_all()[id] + def get(self, loc_id): # pylint: disable=arguments-differ + return self.get_all()[loc_id] # Base URL for fetching data -base_url = "https://facts.csbs.org/covid-19/covid19_county.csv" +BASE_URL = "https://facts.csbs.org/covid-19/covid19_county.csv" @cached(cache=TTLCache(maxsize=1, ttl=3600)) @@ -34,7 +35,7 @@ def get_locations(): :returns: The locations. :rtype: dict """ - request = requests.get(base_url) + request = requests.get(BASE_URL) text = request.text data = list(csv.DictReader(text.splitlines())) @@ -47,11 +48,11 @@ def get_locations(): county = item["County Name"] # Ensure country is specified. - if county == "Unassigned" or county == "Unknown": + if county in {"Unassigned", "Unknown"}: continue # Coordinates. - coordinates = Coordinates(item["Latitude"], item["Longitude"]) + coordinates = Coordinates(item["Latitude"], item["Longitude"]) # pylint: disable=unused-variable # Date string without "EDT" at end. last_update = " ".join(item["Last Update"].split(" ")[0:2]) diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index ef99dddc..0f02409f 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -1,3 +1,4 @@ +"""app.services.location.jhu.py""" import csv from datetime import datetime @@ -21,18 +22,16 @@ def get_all(self): # Get the locations. return get_locations() - def get(self, id): + def get(self, loc_id): # pylint: disable=arguments-differ # Get location at the index equal to provided id. - return self.get_all()[id] + return self.get_all()[loc_id] # --------------------------------------------------------------- -""" -Base URL for fetching category. -""" -base_url = ( +# Base URL for fetching category. +BASE_URL = ( "https://raw.githubusercontent.com/CSSEGISandData/2019-nCoV/master/csse_covid_19_data/csse_covid_19_time_series/" ) @@ -50,7 +49,7 @@ def get_category(category): category = category.lower() # URL to request data from. - url = base_url + "time_series_covid19_%s_global.csv" % category + url = BASE_URL + "time_series_covid19_%s_global.csv" % category # Request the data request = requests.get(url) diff --git a/app/timeline.py b/app/timeline.py index 4916ea2b..0b40d496 100644 --- a/app/timeline.py +++ b/app/timeline.py @@ -1,5 +1,5 @@ +"""app.timeline.py""" from collections import OrderedDict -from datetime import datetime class Timeline: @@ -7,8 +7,8 @@ class Timeline: Timeline with history of data. """ - def __init__(self, history={}): - self.__timeline = history + def __init__(self, history=None): + self.__timeline = history if history else {} @property def timeline(self): @@ -26,7 +26,7 @@ def latest(self): values = list(self.timeline.values()) # Last item is the latest. - if len(values): + if values: return values[-1] or 0 # Fallback value of 0. diff --git a/app/utils/countries.py b/app/utils/countries.py index 6647e679..e0553c64 100644 --- a/app/utils/countries.py +++ b/app/utils/countries.py @@ -1,16 +1,16 @@ +"""app.utils.countries.py""" import logging -from itertools import chain LOGGER = logging.getLogger(__name__) # Default country code. -default_country_code = "XX" +DEFAULT_COUNTRY_CODE = "XX" # Mapping of country names to alpha-2 codes according to # https://en.wikipedia.org/wiki/ISO_3166-1. # As a reference see also https://github.com/TakahikoKawasaki/nv-i18n (in Java) # fmt: off -country_name__country_code = { +COUNTRY_NAME__COUNTRY_CODE = { "Afghanistan" : "AF", "Åland Islands" : "AX", "Albania" : "AL", @@ -363,13 +363,13 @@ } # fmt: on -def country_code(s): +def country_code(value): """ Return two letter country code (Alpha-2) according to https://en.wikipedia.org/wiki/ISO_3166-1 Defaults to "XX". """ - country_code = country_name__country_code.get(s, default_country_code) - if country_code == default_country_code: - LOGGER.warning(f"No country code found for '{s}'. Using '{country_code}'!") + code = COUNTRY_NAME__COUNTRY_CODE.get(value, DEFAULT_COUNTRY_CODE) + if code == DEFAULT_COUNTRY_CODE: + LOGGER.warning(f"No country code found for '{value}'. Using '{code}'!") - return country_code + return code diff --git a/app/utils/date.py b/app/utils/date.py index 3a18832e..5a2cc8e5 100644 --- a/app/utils/date.py +++ b/app/utils/date.py @@ -1,3 +1,4 @@ +"""app.utils.date.py""" from dateutil.parser import parse diff --git a/app/utils/populations.py b/app/utils/populations.py index ea72c334..1d8bd843 100644 --- a/app/utils/populations.py +++ b/app/utils/populations.py @@ -1,19 +1,16 @@ +"""app.utils.populations.py""" import logging -from io import BytesIO, StringIO -from zipfile import ZipFile, ZipInfo import requests -from cachetools import TTLCache, cached - -from .countries import country_code LOGGER = logging.getLogger(__name__) # Fetching of the populations. def fetch_populations(): """ - Returns a dictionary containing the population of each country fetched from the GeoNames (https://www.geonames.org/). - + Returns a dictionary containing the population of each country fetched from the GeoNames. + https://www.geonames.org/ + :returns: The mapping of populations. :rtype: dict """ @@ -34,7 +31,7 @@ def fetch_populations(): # Mapping of alpha-2 codes country codes to population. -populations = fetch_populations() +POPULATIONS = fetch_populations() # Retrieving. def country_population(country_code, default=None): @@ -44,4 +41,4 @@ def country_population(country_code, default=None): :returns: The population. :rtype: int """ - return populations.get(country_code, default) + return POPULATIONS.get(country_code, default) diff --git a/pylintrc b/pylintrc index 4db0f41f..af114a33 100644 --- a/pylintrc +++ b/pylintrc @@ -3,7 +3,7 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. -extension-pkg-whitelist= +extension-pkg-whitelist=pydantic # Add files or directories to the blacklist. They should be base names, not # paths. @@ -139,7 +139,8 @@ disable=print-statement, deprecated-sys-function, exception-escape, comprehension-escape, - bad-continuation # conflicts with black + bad-continuation, # conflicts with black + duplicate-code # turn back on ASAP # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -335,7 +336,7 @@ single-line-if-stmt=no # Format style used to check logging format string. `old` means using % # formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. -logging-format-style=old +logging-format-style=fstr # Logging modules to check that the string format arguments are in logging # function parameter format. @@ -346,8 +347,7 @@ logging-modules=logging # List of note tags to take in consideration, separated by a comma. notes=FIXME, - XXX, - TODO + XXX [SIMILARITIES] diff --git a/tests/test_countries.py b/tests/test_countries.py index 2c9ba65e..e28fb469 100644 --- a/tests/test_countries.py +++ b/tests/test_countries.py @@ -16,8 +16,8 @@ ("Bolivia, Plurinational State of", "BO"), ("Korea, Democratic People's Republic of", "KP"), ("US", "US"), - ("BlaBla", countries.default_country_code), - ("Others", countries.default_country_code), + ("BlaBla", countries.DEFAULT_COUNTRY_CODE), + ("Others", countries.DEFAULT_COUNTRY_CODE), ], ) def test_countries_country_name__country_code(country_name, expected_country_code): From d3ac880363562cb631da1a4f3b94d251012549ce Mon Sep 17 00:00:00 2001 From: Bost Date: Thu, 2 Apr 2020 13:14:39 +0200 Subject: [PATCH 14/38] New alias (#249) Co-authored-by: Rostislav Svoboda --- app/utils/countries.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/utils/countries.py b/app/utils/countries.py index e0553c64..5f926f37 100644 --- a/app/utils/countries.py +++ b/app/utils/countries.py @@ -215,6 +215,7 @@ "Morocco" : "MA", "Mozambique" : "MZ", "Myanmar" : "MM", + "Burma" : "MM", "Namibia" : "NA", "Nauru" : "NR", "Nepal" : "NP", @@ -359,7 +360,10 @@ # "Disputed Territory" : "XX", # "Others" has no mapping, i.e. the default val is used - # "Cruise Ship" has no mapping, i.e. the default val is used + + # ships: + # "Cruise Ship" + # "MS Zaandam" } # fmt: on From 8b55c1637e4c29647168969e2f505fb6180cb9a7 Mon Sep 17 00:00:00 2001 From: James Gray Date: Wed, 1 Apr 2020 17:09:12 +0200 Subject: [PATCH 15/38] Setup a global aiohttp.ClientSession instance --- Pipfile | 1 + app/main.py | 3 +++ app/utils/httputils.py | 30 ++++++++++++++++++++++++++++++ tests/test_httputils.py | 0 4 files changed, 34 insertions(+) create mode 100644 app/utils/httputils.py create mode 100644 tests/test_httputils.py diff --git a/Pipfile b/Pipfile index 2e5b9a1f..84ec24f6 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ pytest = "*" pytest-cov = "*" [packages] +aiohttp = "*" cachetools = "*" fastapi = "*" gunicorn = "*" diff --git a/app/main.py b/app/main.py index 75805ebc..0018f8bf 100644 --- a/app/main.py +++ b/app/main.py @@ -13,6 +13,7 @@ from .data import data_source from .router.v1 import V1 from .router.v2 import V2 +from .utils.httputils import setup_client_session, teardown_client_session # ############ # FastAPI App @@ -28,6 +29,8 @@ version="2.0.1", docs_url="/", redoc_url="/docs", + on_startup=[setup_client_session], + on_shutdown=[teardown_client_session], ) # ##################### diff --git a/app/utils/httputils.py b/app/utils/httputils.py new file mode 100644 index 00000000..de5d5b17 --- /dev/null +++ b/app/utils/httputils.py @@ -0,0 +1,30 @@ +import logging + +from aiohttp import ClientSession + + +# Singleton aiohttp.ClientSession instance. +client_session: ClientSession + + +LOGGER = logging.getLogger(__name__) + + +async def setup_client_session(): + """Set up the application-global aiohttp.ClientSession instance. + + aiohttp recommends that only one ClientSession exist for the lifetime of an application. + See: https://docs.aiohttp.org/en/stable/client_quickstart.html#make-a-request + + """ + global client_session + LOGGER.info("Setting up global aiohttp.ClientSession.") + client_session = ClientSession() + + +async def teardown_client_session(): + """Close the application-global aiohttp.ClientSession. + """ + global client_session + LOGGER.info("Closing global aiohttp.ClientSession.") + await client_session.close() diff --git a/tests/test_httputils.py b/tests/test_httputils.py new file mode 100644 index 00000000..e69de29b From 63f68841602b38bba3fa91a58f39608aae622ce4 Mon Sep 17 00:00:00 2001 From: James Gray Date: Wed, 1 Apr 2020 17:11:32 +0200 Subject: [PATCH 16/38] Make core blocking functions async using aiohttp --- app/services/location/csbs.py | 8 ++++---- app/services/location/jhu.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index 84654963..32aa429c 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -2,11 +2,11 @@ import csv from datetime import datetime -import requests from cachetools import TTLCache, cached from ...coordinates import Coordinates from ...location.csbs import CSBSLocation +from ...utils import httputils from . import LocationService @@ -28,15 +28,15 @@ def get(self, loc_id): # pylint: disable=arguments-differ @cached(cache=TTLCache(maxsize=1, ttl=3600)) -def get_locations(): +async 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 + async with httputils.client_session.get(BASE_URL) as response: + text = await response.text() data = list(csv.DictReader(text.splitlines())) diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index 0f02409f..72783ee4 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -2,7 +2,6 @@ import csv from datetime import datetime -import requests from cachetools import TTLCache, cached from ...coordinates import Coordinates @@ -10,6 +9,7 @@ from ...timeline import Timeline from ...utils import countries from ...utils import date as date_util +from ...utils import httputils from . import LocationService @@ -37,7 +37,7 @@ def get(self, loc_id): # pylint: disable=arguments-differ @cached(cache=TTLCache(maxsize=1024, ttl=3600)) -def get_category(category): +async def get_category(category): """ Retrieves the data for the provided category. The data is cached for 1 hour. @@ -52,8 +52,8 @@ def get_category(category): url = BASE_URL + "time_series_covid19_%s_global.csv" % category # Request the data - request = requests.get(url) - text = request.text + async with httputils.client_session.get(url) as response: + text = await response.text() # Parse the CSV. data = list(csv.DictReader(text.splitlines())) From 9b945738b5a33a12710c62acf74651d54080d06e Mon Sep 17 00:00:00 2001 From: James Gray Date: Wed, 1 Apr 2020 17:12:10 +0200 Subject: [PATCH 17/38] Add async/await keywords as necessary up the call chain --- app/router/v1/all.py | 8 ++++---- app/router/v1/confirmed.py | 6 ++++-- app/router/v1/deaths.py | 6 ++++-- app/router/v1/recovered.py | 6 ++++-- app/router/v2/latest.py | 4 ++-- app/router/v2/locations.py | 9 +++++---- app/services/location/__init__.py | 4 ++-- app/services/location/csbs.py | 15 +++++++++------ app/services/location/jhu.py | 30 ++++++++++++++++++------------ 9 files changed, 52 insertions(+), 36 deletions(-) diff --git a/app/router/v1/all.py b/app/router/v1/all.py index b26fe25b..91b9e826 100644 --- a/app/router/v1/all.py +++ b/app/router/v1/all.py @@ -4,11 +4,11 @@ @V1.get("/all") -def all(): # pylint: disable=redefined-builtin +async def all(): # pylint: disable=redefined-builtin """Get all the categories.""" - confirmed = get_category("confirmed") - deaths = get_category("deaths") - recovered = get_category("recovered") + confirmed = await get_category("confirmed") + deaths = await get_category("deaths") + recovered = await get_category("recovered") return { # Data. diff --git a/app/router/v1/confirmed.py b/app/router/v1/confirmed.py index f3b97523..eda7702e 100644 --- a/app/router/v1/confirmed.py +++ b/app/router/v1/confirmed.py @@ -4,6 +4,8 @@ @V1.get("/confirmed") -def confirmed(): +async def confirmed(): """Confirmed cases.""" - return get_category("confirmed") + confirmed = await get_category("confirmed") + + return confirmed diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py index 65ed0967..d41d1d9d 100644 --- a/app/router/v1/deaths.py +++ b/app/router/v1/deaths.py @@ -4,6 +4,8 @@ @V1.get("/deaths") -def deaths(): +async def deaths(): """Total deaths.""" - return get_category("deaths") + deaths = await get_category("deaths") + + return deaths diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py index 254823ed..24fcb4fd 100644 --- a/app/router/v1/recovered.py +++ b/app/router/v1/recovered.py @@ -4,6 +4,8 @@ @V1.get("/recovered") -def recovered(): +async def recovered(): """Recovered cases.""" - return get_category("recovered") + recovered = await get_category("recovered") + + return recovered diff --git a/app/router/v2/latest.py b/app/router/v2/latest.py index 071c3a22..105b16fe 100644 --- a/app/router/v2/latest.py +++ b/app/router/v2/latest.py @@ -7,11 +7,11 @@ @V2.get("/latest", response_model=Latest) -def get_latest(request: Request, source: Sources = "jhu"): # pylint: disable=unused-argument +async def get_latest(request: Request, source: Sources = "jhu"): # pylint: disable=unused-argument """ Getting latest amount of total confirmed cases, deaths, and recoveries. """ - locations = request.state.source.get_all() + locations = await request.state.source.get_all() return { "latest": { "confirmed": sum(map(lambda location: location.confirmed, locations)), diff --git a/app/router/v2/locations.py b/app/router/v2/locations.py index 815b1eb8..649f9c9e 100644 --- a/app/router/v2/locations.py +++ b/app/router/v2/locations.py @@ -9,7 +9,7 @@ # pylint: disable=unused-argument,too-many-arguments,redefined-builtin @V2.get("/locations", response_model=Locations, response_model_exclude_unset=True) -def get_locations( +async def get_locations( request: Request, source: Sources = "jhu", country_code: str = None, @@ -28,7 +28,7 @@ def get_locations( params.pop("timelines", None) # Retrieve all the locations. - locations = request.state.source.get_all() + locations = await request.state.source.get_all() # Attempt to filter out locations with properties matching the provided query params. for key, value in params.items(): @@ -57,8 +57,9 @@ def get_locations( # pylint: disable=invalid-name @V2.get("/locations/{id}", response_model=Location) -def get_location_by_id(request: Request, id: int, source: Sources = "jhu", timelines: bool = True): +async def get_location_by_id(request: Request, id: int, source: Sources = "jhu", timelines: bool = True): """ Getting specific location by id. """ - return {"location": request.state.source.get(id).serialize(timelines)} + location = await request.state.source.get(id) + return {"location": location.serialize(timelines)} diff --git a/app/services/location/__init__.py b/app/services/location/__init__.py index 404e9f7e..6d292b54 100644 --- a/app/services/location/__init__.py +++ b/app/services/location/__init__.py @@ -8,7 +8,7 @@ class LocationService(ABC): """ @abstractmethod - def get_all(self): + async def get_all(self): """ Gets and returns all of the locations. @@ -18,7 +18,7 @@ def get_all(self): raise NotImplementedError @abstractmethod - def get(self, id): # pylint: disable=redefined-builtin,invalid-name + async def get(self, id): # pylint: disable=redefined-builtin,invalid-name """ Gets and returns location with the provided id. diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index 32aa429c..8a1ded3e 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -12,15 +12,18 @@ class CSBSLocationService(LocationService): """ - Servive for retrieving locations from csbs + Service for retrieving locations from csbs """ - def get_all(self): - # Get the locations - return get_locations() + async def get_all(self): + # Get the locations. + locations = await get_locations() + return locations - def get(self, loc_id): # pylint: disable=arguments-differ - return self.get_all()[loc_id] + async def get(self, loc_id): # pylint: disable=arguments-differ + # Get location at the index equal to the provided id. + locations = await self.get_all() + return locations[loc_id] # Base URL for fetching data diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index 72783ee4..c050d111 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -18,13 +18,15 @@ class JhuLocationService(LocationService): Service for retrieving locations from Johns Hopkins CSSE (https://github.com/CSSEGISandData/COVID-19). """ - def get_all(self): + async def get_all(self): # Get the locations. - return get_locations() + locations = await get_locations() + return locations - def get(self, loc_id): # pylint: disable=arguments-differ + async def get(self, loc_id): # pylint: disable=arguments-differ # Get location at the index equal to provided id. - return self.get_all()[loc_id] + locations = await self.get_all() + return locations[loc_id] # --------------------------------------------------------------- @@ -103,7 +105,7 @@ async def get_category(category): @cached(cache=TTLCache(maxsize=1024, ttl=3600)) -def get_locations(): +async def get_locations(): """ Retrieves the locations from the categories. The locations are cached for 1 hour. @@ -111,20 +113,24 @@ def get_locations(): :rtype: List[Location] """ # Get all of the data categories locations. - confirmed = get_category("confirmed")["locations"] - deaths = get_category("deaths")["locations"] - # recovered = get_category('recovered')['locations'] + confirmed = await get_category("confirmed") + deaths = await get_category("deaths") + # recovered = await get_category("recovered") + + locations_confirmed = confirmed["locations"] + locations_deaths = deaths["locations"] + # locations_recovered = recovered["locations"] # Final locations to return. locations = [] # Go through locations. - for index, location in enumerate(confirmed): + for index, location in enumerate(locations_confirmed): # Get the timelines. timelines = { - "confirmed": confirmed[index]["history"], - "deaths": deaths[index]["history"], - # 'recovered' : recovered[index]['history'], + "confirmed": locations_confirmed[index]["history"], + "deaths": locations_deaths[index]["history"], + # 'recovered' : locations_recovered[index]['history'], } # Grab coordinates. From 2f40cea8fa190cebd0a80a03fb9a3982f6e1db74 Mon Sep 17 00:00:00 2001 From: James Gray Date: Wed, 1 Apr 2020 17:13:09 +0200 Subject: [PATCH 18/38] Use asyncache to cache results of asyncio coroutines See: https://github.com/hephex/asyncache --- Pipfile | 1 + app/services/location/csbs.py | 3 ++- app/services/location/jhu.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 84ec24f6..a2261aea 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ pytest-cov = "*" [packages] aiohttp = "*" +asyncache = "*" cachetools = "*" fastapi = "*" gunicorn = "*" diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index 8a1ded3e..95f1c4b2 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -2,7 +2,8 @@ import csv from datetime import datetime -from cachetools import TTLCache, cached +from asyncache import cached +from cachetools import TTLCache from ...coordinates import Coordinates from ...location.csbs import CSBSLocation diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index c050d111..adee6bdc 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -2,7 +2,8 @@ import csv from datetime import datetime -from cachetools import TTLCache, cached +from asyncache import cached +from cachetools import TTLCache from ...coordinates import Coordinates from ...location import TimelinedLocation From 63675dc96364bd0406342afa40e646584879d599 Mon Sep 17 00:00:00 2001 From: James Gray Date: Thu, 2 Apr 2020 11:49:58 +0200 Subject: [PATCH 19/38] Add test dependencies These are required to be able to test asyncio functionality, as well as making ASGI test clients play nice asynchronously. --- Pipfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Pipfile b/Pipfile index a2261aea..d5097194 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] +async-asgi-testclient = "*" bandit = "*" black = "==19.10b0" coveralls = "*" @@ -11,6 +12,7 @@ invoke = "*" isort = "*" pylint = "*" pytest = "*" +pytest-asyncio = "*" pytest-cov = "*" [packages] From 0e11daa44b8aeee1c700c9f5215092f4698e4446 Mon Sep 17 00:00:00 2001 From: James Gray Date: Thu, 2 Apr 2020 11:49:04 +0200 Subject: [PATCH 20/38] Update test_jhu to handle asyncio - Move test fixtures into tests/fixtures.py - Update test fixtures to mock aiohttp.ClientSession.get, instead of requests.get - Add a context manager to replace the global httputils.client_session with an AsyncMock - Misc. cleanup --- tests/fixtures.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_jhu.py | 83 ++++++++----------------------------------- 2 files changed, 103 insertions(+), 69 deletions(-) create mode 100644 tests/fixtures.py diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 00000000..faea895f --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,89 @@ +import datetime +import os +from contextlib import asynccontextmanager +from unittest import mock + +from app.utils import httputils + + +class DateTimeStrpTime: + """Returns instance of `DateTimeStrpTime` + when calling `app.services.location.jhu.datetime.trptime(date, '%m/%d/%y').isoformat()`. + """ + + def __init__(self, date, strformat): + self.date = date + self.strformat = strformat + + def isoformat(self): + return datetime.datetime.strptime(self.date, self.strformat).isoformat() + + +class FakeRequestsGetResponse: + """Fake instance of a response from `aiohttp.ClientSession.get`. + """ + + def __init__(self, url, filename, state): + self.url = url + self.filename = filename + self.state = state + + async def text(self): + return self.read_file(self.state) + + def read_file(self, state): + """ + Mock HTTP GET-method and return text from file + """ + state = state.lower() + + # Determine filepath. + filepath = os.path.join(os.path.dirname(__file__), "example_data/{}.csv".format(state)) + + # Return fake response. + print("Try to read {}".format(filepath)) + with open(filepath, "r") as file: + return file.read() + + +@asynccontextmanager +async def mock_client_session(): + """Context manager that replaces the global client_session with an AsyncMock instance. + + :Example: + + >>> async with mock_client_session() as mocked_client_session: + >>> mocked_client_session.get = mocked_session_get + >>> # test code... + + """ + + httputils.client_session = mocked_client_session = mock.AsyncMock() + try: + yield mocked_client_session + finally: + del httputils.client_session + + +@asynccontextmanager +async def mocked_session_get(*args, **kwargs): + """Mock response from client_session.get. + """ + + url = args[0] + filename = url.split("/")[-1] + + # clean up for id token (e.g. Deaths) + state = filename.split("-")[-1].replace(".csv", "").lower().capitalize() + + yield FakeRequestsGetResponse(url, filename, state) + + +def mocked_strptime_isoformat(*args, **kwargs): + """Mock return value from datetime.strptime().isoformat(). + """ + + date = args[0] + strformat = args[1] + + return DateTimeStrpTime(date, strformat) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index f9c214a6..4f46becf 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -6,81 +6,26 @@ import app from app import location from app.services.location import jhu -from app.utils import date +from tests.fixtures import mock_client_session +from tests.fixtures import mocked_session_get +from tests.fixtures import mocked_strptime_isoformat DATETIME_STRING = "2020-03-17T10:23:22.505550" -def mocked_requests_get(*args, **kwargs): - class FakeRequestsGetResponse: - """ - Returns instance of `FakeRequestsGetResponse` - when calling `app.services.location.jhu.requests.get()` - """ - - def __init__(self, url, filename, state): - self.url = url - self.filename = filename - self.state = state - self.text = self.read_file(self.state) - - def read_file(self, state): - """ - Mock HTTP GET-method and return text from file - """ - state = state.lower() - - # Determine filepath. - filepath = "tests/example_data/{}.csv".format(state) - - # Return fake response. - print("Try to read {}".format(filepath)) - with open(filepath, "r") as file: - return file.read() - - # get url from `request.get` - url = args[0] - - # get filename from url - filename = url.split("/")[-1] - - # clean up for id token (e.g. Deaths) - state = filename.split("-")[-1].replace(".csv", "").lower().capitalize() - - return FakeRequestsGetResponse(url, filename, state) - - -def mocked_strptime_isoformat(*args, **kwargs): - class DateTimeStrpTime: - """ - Returns instance of `DateTimeStrpTime` - when calling `app.services.location.jhu.datetime.trptime(date, '%m/%d/%y').isoformat()` - """ - - def __init__(self, date, strformat): - self.date = date - self.strformat = strformat - - def isoformat(self): - return datetime.datetime.strptime(self.date, self.strformat).isoformat() - - date = args[0] - strformat = args[1] - - return DateTimeStrpTime(date, strformat) - - +@pytest.mark.asyncio @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): - # mock app.services.location.jhu.datetime.utcnow().isoformat() +async def test_get_locations(mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat - output = jhu.get_locations() - assert isinstance(output, list) - assert isinstance(output[0], location.Location) + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + output = await jhu.get_locations() + + assert isinstance(output, list) + assert isinstance(output[0], location.Location) - # `jhu.get_locations()` creates id based on confirmed list - location_confirmed = jhu.get_category("confirmed") - assert len(output) == len(location_confirmed["locations"]) + # `jhu.get_locations()` creates id based on confirmed list + location_confirmed = await jhu.get_category("confirmed") + assert len(output) == len(location_confirmed["locations"]) From 3bfc94eff8caa84291507e3f24caf90003dfd06c Mon Sep 17 00:00:00 2001 From: James Gray Date: Thu, 2 Apr 2020 11:50:37 +0200 Subject: [PATCH 21/38] Update test_csbs to handle asyncio --- ...sample_covid19_county.csv => covid19_county.csv} | 0 tests/test_csbs.py | 13 +++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) rename tests/example_data/{sample_covid19_county.csv => covid19_county.csv} (100%) diff --git a/tests/example_data/sample_covid19_county.csv b/tests/example_data/covid19_county.csv similarity index 100% rename from tests/example_data/sample_covid19_county.csv rename to tests/example_data/covid19_county.csv diff --git a/tests/test_csbs.py b/tests/test_csbs.py index 64852102..087e483a 100644 --- a/tests/test_csbs.py +++ b/tests/test_csbs.py @@ -5,6 +5,8 @@ import app from app.services.location import csbs +from tests.fixtures import mock_client_session +from tests.fixtures import mocked_session_get def mocked_csbs_requests_get(*args, **kwargs): @@ -21,7 +23,7 @@ def read_file(self): """ Mock HTTP GET-method and return text from file """ - filepath = "tests/example_data/sample_covid19_county.csv" + filepath = "tests/example_data/covid19_county.csv" print("Try to read {}".format(filepath)) with open(filepath, "r") as file: return file.read() @@ -29,9 +31,12 @@ def read_file(self): 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() +@pytest.mark.asyncio +async def test_get_locations(): + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + data = await csbs.get_locations() + assert isinstance(data, list) # check to see that Unknown/Unassigned has been filtered From c4a6c9ed2ba19a96f173fb6a07d445d31bd0cbde Mon Sep 17 00:00:00 2001 From: James Gray Date: Thu, 2 Apr 2020 11:50:54 +0200 Subject: [PATCH 22/38] Update test_routes to handle asyncio --- tests/test_routes.py | 90 ++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 29 deletions(-) diff --git a/tests/test_routes.py b/tests/test_routes.py index 48d804e5..fa657ed1 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -4,17 +4,17 @@ from unittest import mock import pytest -from fastapi.testclient import TestClient +from async_asgi_testclient import TestClient -# import app -# from app import services +from .fixtures import mock_client_session +from .fixtures import mocked_session_get +from .fixtures import mocked_strptime_isoformat +from .test_jhu import DATETIME_STRING from app.main import APP -from .test_jhu import DATETIME_STRING, mocked_requests_get, mocked_strptime_isoformat - +@pytest.mark.asyncio @mock.patch("app.services.location.jhu.datetime") -@mock.patch("app.services.location.jhu.requests.get", side_effect=mocked_requests_get) class FlaskRoutesTest(unittest.TestCase): """ Need to mock some objects to control testing data locally @@ -32,89 +32,112 @@ def read_file_v1(self, state): expected_json_output = file.read() return expected_json_output - def test_root_api(self, mock_request_get, mock_datetime): + async def test_root_api(self, mock_datetime): """Validate that / returns a 200 and is not a redirect.""" - response = self.asgi_client.get("/") + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await self.asgi_client.get("/") assert response.status_code == 200 assert not response.is_redirect - def test_v1_confirmed(self, mock_request_get, mock_datetime): + async def test_v1_confirmed(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "confirmed" expected_json_output = self.read_file_v1(state=state) - return_data = self.asgi_client.get("/{}".format(state)).json() + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await self.asgi_client.get("/{}".format(state)) + return_data = response.json() assert return_data == json.loads(expected_json_output) - def test_v1_deaths(self, mock_request_get, mock_datetime): + async def test_v1_deaths(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "deaths" expected_json_output = self.read_file_v1(state=state) - return_data = self.asgi_client.get("/{}".format(state)).json() + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await self.asgi_client.get("/{}".format(state)) + return_data = response.json() assert return_data == json.loads(expected_json_output) - def test_v1_recovered(self, mock_request_get, mock_datetime): + async def test_v1_recovered(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "recovered" expected_json_output = self.read_file_v1(state=state) - return_data = self.asgi_client.get("/{}".format(state)).json() + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await self.asgi_client.get("/{}".format(state)) + return_data = response.json() assert return_data == json.loads(expected_json_output) - def test_v1_all(self, mock_request_get, mock_datetime): + async def test_v1_all(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "all" expected_json_output = self.read_file_v1(state=state) - return_data = self.asgi_client.get("/{}".format(state)).json() + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await self.asgi_client.get("/{}".format(state)) + return_data = response.json() assert return_data == json.loads(expected_json_output) - def test_v2_latest(self, mock_request_get, mock_datetime): + async def test_v2_latest(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "latest" - return_data = self.asgi_client.get(f"/v2/{state}").json() + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await self.asgi_client.get(f"/v2/{state}") + return_data = response.json() check_dict = {"latest": {"confirmed": 1940, "deaths": 1940, "recovered": 0}} assert return_data == check_dict - def test_v2_locations(self, mock_request_get, mock_datetime): + async def test_v2_locations(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "locations" - return_data = self.asgi_client.get("/v2/{}".format(state)).json() + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await self.asgi_client.get("/v2/{}".format(state)) + return_data = response.json() filepath = "tests/expected_output/v2_{state}.json".format(state=state) with open(filepath, "r") as file: expected_json_output = file.read() + # TODO: Why is this failing? # assert return_data == json.loads(expected_json_output) - def test_v2_locations_id(self, mock_request_get, mock_datetime): + async def test_v2_locations_id(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat state = "locations" test_id = 1 - return_data = self.asgi_client.get("/v2/{}/{}".format(state, test_id)).json() + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await self.asgi_client.get("/v2/{}/{}".format(state, test_id)) + return_data = response.json() filepath = "tests/expected_output/v2_{state}_id_{test_id}.json".format(state=state, test_id=test_id) with open(filepath, "r") as file: expected_json_output = file.read() + # TODO: Why is this failing? # assert return_data == expected_json_output - def tearDown(self): - pass - +@pytest.mark.asyncio @pytest.mark.parametrize( "query_params,expected_status", [ @@ -128,13 +151,18 @@ def tearDown(self): ({"source": "jhu", "country_code": "US"}, 404), ], ) -def test_locations_status_code(api_client, query_params, expected_status): - response = api_client.get("/v2/locations", params=query_params) +async def test_locations_status_code(query_params, expected_status): + api_client = TestClient(APP) + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await api_client.get("/v2/locations", query_string=query_params) + print(f"GET {response.url}\n{response}") print(f"\tjson:\n{pf(response.json())[:1000]}\n\t...") assert response.status_code == expected_status +@pytest.mark.asyncio @pytest.mark.parametrize( "query_params", [ @@ -146,8 +174,12 @@ def test_locations_status_code(api_client, query_params, expected_status): {"source": "jhu", "timelines": True}, ], ) -def test_latest(api_client, query_params): - response = api_client.get("/v2/latest", params=query_params) +async def test_latest(query_params): + api_client = TestClient(APP) + async with mock_client_session() as mocked_client_session: + mocked_client_session.get = mocked_session_get + response = await api_client.get("/v2/latest", query_string=query_params) + print(f"GET {response.url}\n{response}") response_json = response.json() From 5b798c59a29bd9bb27d19c08fceb10373ba61add Mon Sep 17 00:00:00 2001 From: James Gray Date: Thu, 2 Apr 2020 11:51:12 +0200 Subject: [PATCH 23/38] Remove unused imports --- tests/test_csbs.py | 4 ---- tests/test_jhu.py | 2 -- 2 files changed, 6 deletions(-) diff --git a/tests/test_csbs.py b/tests/test_csbs.py index 087e483a..255125b1 100644 --- a/tests/test_csbs.py +++ b/tests/test_csbs.py @@ -1,9 +1,5 @@ -import datetime -from unittest import mock - import pytest -import app from app.services.location import csbs from tests.fixtures import mock_client_session from tests.fixtures import mocked_session_get diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 4f46becf..80c17ee2 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -1,9 +1,7 @@ -import datetime from unittest import mock import pytest -import app from app import location from app.services.location import jhu from tests.fixtures import mock_client_session From 711ad5f3a03e726f23d0c1603f2446f7cdbaae26 Mon Sep 17 00:00:00 2001 From: James Gray Date: Thu, 2 Apr 2020 11:55:58 +0200 Subject: [PATCH 24/38] Ignore intelliJ meta folder --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index efd5545c..9c41818c 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,7 @@ docs/_build/ target/ # OSX Stuff -.DS_Store \ No newline at end of file +.DS_Store + +# IntelliJ/Pycharm +.idea/ From 0e827fb37308edf6d01cac364d1ada868ae899e3 Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 12:20:23 +0200 Subject: [PATCH 25/38] Add an async_api_client pytest fixture --- tests/conftest.py | 11 ++++++++++- tests/test_routes.py | 10 ++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a9811d22..22c33432 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ """ import pytest from fastapi.testclient import TestClient +from async_asgi_testclient import TestClient as AsyncTestClient from app.main import APP @@ -12,7 +13,15 @@ @pytest.fixture def api_client(): """ - Returns a TestClient. + Returns a fastapi.testclient.TestClient. The test client uses the requests library for making http requests. """ return TestClient(APP) + + +@pytest.fixture +async def async_api_client(): + """ + Returns an async_asgi_testclient.TestClient. + """ + return AsyncTestClient(APP) diff --git a/tests/test_routes.py b/tests/test_routes.py index fa657ed1..17112666 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -151,11 +151,10 @@ async def test_v2_locations_id(self, mock_datetime): ({"source": "jhu", "country_code": "US"}, 404), ], ) -async def test_locations_status_code(query_params, expected_status): - api_client = TestClient(APP) +async def test_locations_status_code(async_api_client, query_params, expected_status): async with mock_client_session() as mocked_client_session: mocked_client_session.get = mocked_session_get - response = await api_client.get("/v2/locations", query_string=query_params) + response = await async_api_client.get("/v2/locations", query_string=query_params) print(f"GET {response.url}\n{response}") print(f"\tjson:\n{pf(response.json())[:1000]}\n\t...") @@ -174,11 +173,10 @@ async def test_locations_status_code(query_params, expected_status): {"source": "jhu", "timelines": True}, ], ) -async def test_latest(query_params): - api_client = TestClient(APP) +async def test_latest(async_api_client, query_params): async with mock_client_session() as mocked_client_session: mocked_client_session.get = mocked_session_get - response = await api_client.get("/v2/latest", query_string=query_params) + response = await async_api_client.get("/v2/latest", query_string=query_params) print(f"GET {response.url}\n{response}") From afca2a934130652d0d8152595387bfd2a4fdbbc5 Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 12:21:40 +0200 Subject: [PATCH 26/38] Consolidate fixtures into conftest.py I didn't see this file when I first added the new fixtures. --- tests/conftest.py | 91 +++++++++++++++++++++++++++++++++++++++++++- tests/fixtures.py | 89 ------------------------------------------- tests/test_csbs.py | 4 +- tests/test_jhu.py | 6 +-- tests/test_routes.py | 6 +-- 5 files changed, 98 insertions(+), 98 deletions(-) delete mode 100644 tests/fixtures.py diff --git a/tests/conftest.py b/tests/conftest.py index 22c33432..39f1d15f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,11 +3,17 @@ Global conftest file for shared pytest fixtures """ +import datetime +import os +from contextlib import asynccontextmanager +from unittest import mock + import pytest -from fastapi.testclient import TestClient from async_asgi_testclient import TestClient as AsyncTestClient +from fastapi.testclient import TestClient from app.main import APP +from app.utils import httputils @pytest.fixture @@ -25,3 +31,86 @@ async def async_api_client(): Returns an async_asgi_testclient.TestClient. """ return AsyncTestClient(APP) + + +class DateTimeStrpTime: + """Returns instance of `DateTimeStrpTime` + when calling `app.services.location.jhu.datetime.trptime(date, '%m/%d/%y').isoformat()`. + """ + + def __init__(self, date, strformat): + self.date = date + self.strformat = strformat + + def isoformat(self): + return datetime.datetime.strptime(self.date, self.strformat).isoformat() + + +class FakeRequestsGetResponse: + """Fake instance of a response from `aiohttp.ClientSession.get`. + """ + + def __init__(self, url, filename, state): + self.url = url + self.filename = filename + self.state = state + + async def text(self): + return self.read_file(self.state) + + def read_file(self, state): + """ + Mock HTTP GET-method and return text from file + """ + state = state.lower() + + # Determine filepath. + filepath = os.path.join(os.path.dirname(__file__), "example_data/{}.csv".format(state)) + + # Return fake response. + print("Try to read {}".format(filepath)) + with open(filepath, "r") as file: + return file.read() + + +@asynccontextmanager +async def mock_client_session(): + """Context manager that replaces the global client_session with an AsyncMock instance. + + :Example: + + >>> async with mock_client_session() as mocked_client_session: + >>> mocked_client_session.get = mocked_session_get + >>> # test code... + + """ + + httputils.client_session = mocked_client_session = mock.AsyncMock() + try: + yield mocked_client_session + finally: + del httputils.client_session + + +@asynccontextmanager +async def mocked_session_get(*args, **kwargs): + """Mock response from client_session.get. + """ + + url = args[0] + filename = url.split("/")[-1] + + # clean up for id token (e.g. Deaths) + state = filename.split("-")[-1].replace(".csv", "").lower().capitalize() + + yield FakeRequestsGetResponse(url, filename, state) + + +def mocked_strptime_isoformat(*args, **kwargs): + """Mock return value from datetime.strptime().isoformat(). + """ + + date = args[0] + strformat = args[1] + + return DateTimeStrpTime(date, strformat) diff --git a/tests/fixtures.py b/tests/fixtures.py deleted file mode 100644 index faea895f..00000000 --- a/tests/fixtures.py +++ /dev/null @@ -1,89 +0,0 @@ -import datetime -import os -from contextlib import asynccontextmanager -from unittest import mock - -from app.utils import httputils - - -class DateTimeStrpTime: - """Returns instance of `DateTimeStrpTime` - when calling `app.services.location.jhu.datetime.trptime(date, '%m/%d/%y').isoformat()`. - """ - - def __init__(self, date, strformat): - self.date = date - self.strformat = strformat - - def isoformat(self): - return datetime.datetime.strptime(self.date, self.strformat).isoformat() - - -class FakeRequestsGetResponse: - """Fake instance of a response from `aiohttp.ClientSession.get`. - """ - - def __init__(self, url, filename, state): - self.url = url - self.filename = filename - self.state = state - - async def text(self): - return self.read_file(self.state) - - def read_file(self, state): - """ - Mock HTTP GET-method and return text from file - """ - state = state.lower() - - # Determine filepath. - filepath = os.path.join(os.path.dirname(__file__), "example_data/{}.csv".format(state)) - - # Return fake response. - print("Try to read {}".format(filepath)) - with open(filepath, "r") as file: - return file.read() - - -@asynccontextmanager -async def mock_client_session(): - """Context manager that replaces the global client_session with an AsyncMock instance. - - :Example: - - >>> async with mock_client_session() as mocked_client_session: - >>> mocked_client_session.get = mocked_session_get - >>> # test code... - - """ - - httputils.client_session = mocked_client_session = mock.AsyncMock() - try: - yield mocked_client_session - finally: - del httputils.client_session - - -@asynccontextmanager -async def mocked_session_get(*args, **kwargs): - """Mock response from client_session.get. - """ - - url = args[0] - filename = url.split("/")[-1] - - # clean up for id token (e.g. Deaths) - state = filename.split("-")[-1].replace(".csv", "").lower().capitalize() - - yield FakeRequestsGetResponse(url, filename, state) - - -def mocked_strptime_isoformat(*args, **kwargs): - """Mock return value from datetime.strptime().isoformat(). - """ - - date = args[0] - strformat = args[1] - - return DateTimeStrpTime(date, strformat) diff --git a/tests/test_csbs.py b/tests/test_csbs.py index 255125b1..a018b98e 100644 --- a/tests/test_csbs.py +++ b/tests/test_csbs.py @@ -1,8 +1,8 @@ import pytest from app.services.location import csbs -from tests.fixtures import mock_client_session -from tests.fixtures import mocked_session_get +from tests.conftest import mock_client_session +from tests.conftest import mocked_session_get def mocked_csbs_requests_get(*args, **kwargs): diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 80c17ee2..1ef36f37 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -4,9 +4,9 @@ from app import location from app.services.location import jhu -from tests.fixtures import mock_client_session -from tests.fixtures import mocked_session_get -from tests.fixtures import mocked_strptime_isoformat +from tests.conftest import mock_client_session +from tests.conftest import mocked_session_get +from tests.conftest import mocked_strptime_isoformat DATETIME_STRING = "2020-03-17T10:23:22.505550" diff --git a/tests/test_routes.py b/tests/test_routes.py index 17112666..f7623965 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -6,9 +6,9 @@ import pytest from async_asgi_testclient import TestClient -from .fixtures import mock_client_session -from .fixtures import mocked_session_get -from .fixtures import mocked_strptime_isoformat +from .conftest import mock_client_session +from .conftest import mocked_session_get +from .conftest import mocked_strptime_isoformat from .test_jhu import DATETIME_STRING from app.main import APP From 3dfe15f34c5f4f216caffb66a67b11a7b8ccd21a Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 12:37:55 +0200 Subject: [PATCH 27/38] Make mock_client_session a proper Pytest fixture --- tests/conftest.py | 26 +++++++++----- tests/test_csbs.py | 8 ++--- tests/test_jhu.py | 18 +++++----- tests/test_routes.py | 86 +++++++++++++++++++++++--------------------- 4 files changed, 73 insertions(+), 65 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 39f1d15f..9fde87d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -73,21 +73,29 @@ def read_file(self, state): return file.read() -@asynccontextmanager -async def mock_client_session(): - """Context manager that replaces the global client_session with an AsyncMock instance. +@pytest.fixture(scope="class") +def mock_client_session_class(request): + """Class fixture to expose an AsyncMock to unittest.TestCase subclasses. - :Example: + See: https://docs.pytest.org/en/5.4.1/unittest.html#mixing-pytest-fixtures-into-unittest-testcase-subclasses-using-marks + """ + + httputils.client_session = request.cls.mock_client_session = mock.AsyncMock() + try: + yield + finally: + del httputils.client_session - >>> async with mock_client_session() as mocked_client_session: - >>> mocked_client_session.get = mocked_session_get - >>> # test code... +@pytest.fixture +async def mock_client_session(): + """Context manager fixture that replaces the global client_session with an AsyncMock + instance. """ - httputils.client_session = mocked_client_session = mock.AsyncMock() + httputils.client_session = mock.AsyncMock() try: - yield mocked_client_session + yield httputils.client_session finally: del httputils.client_session diff --git a/tests/test_csbs.py b/tests/test_csbs.py index a018b98e..912bdd11 100644 --- a/tests/test_csbs.py +++ b/tests/test_csbs.py @@ -1,7 +1,6 @@ import pytest from app.services.location import csbs -from tests.conftest import mock_client_session from tests.conftest import mocked_session_get @@ -28,10 +27,9 @@ def read_file(self): @pytest.mark.asyncio -async def test_get_locations(): - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - data = await csbs.get_locations() +async def test_get_locations(mock_client_session): + mock_client_session.get = mocked_session_get + data = await csbs.get_locations() assert isinstance(data, list) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 1ef36f37..12a3f704 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -4,7 +4,6 @@ from app import location from app.services.location import jhu -from tests.conftest import mock_client_session from tests.conftest import mocked_session_get from tests.conftest import mocked_strptime_isoformat @@ -13,17 +12,16 @@ @pytest.mark.asyncio @mock.patch("app.services.location.jhu.datetime") -async def test_get_locations(mock_datetime): +async def test_get_locations(mock_datetime, mock_client_session): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - output = await jhu.get_locations() + mock_client_session.get = mocked_session_get + output = await jhu.get_locations() - assert isinstance(output, list) - assert isinstance(output[0], location.Location) + assert isinstance(output, list) + assert isinstance(output[0], location.Location) - # `jhu.get_locations()` creates id based on confirmed list - location_confirmed = await jhu.get_category("confirmed") - assert len(output) == len(location_confirmed["locations"]) + # `jhu.get_locations()` creates id based on confirmed list + location_confirmed = await jhu.get_category("confirmed") + assert len(output) == len(location_confirmed["locations"]) diff --git a/tests/test_routes.py b/tests/test_routes.py index f7623965..0f38c353 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -6,15 +6,14 @@ import pytest from async_asgi_testclient import TestClient -from .conftest import mock_client_session from .conftest import mocked_session_get from .conftest import mocked_strptime_isoformat from .test_jhu import DATETIME_STRING from app.main import APP +@pytest.mark.usefixtures("mock_client_session_class") @pytest.mark.asyncio -@mock.patch("app.services.location.jhu.datetime") class FlaskRoutesTest(unittest.TestCase): """ Need to mock some objects to control testing data locally @@ -32,84 +31,91 @@ def read_file_v1(self, state): expected_json_output = file.read() return expected_json_output + @mock.patch("app.services.location.jhu.datetime") async def test_root_api(self, mock_datetime): """Validate that / returns a 200 and is not a redirect.""" - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await self.asgi_client.get("/") + self.mock_client_session.get = mocked_session_get + + response = await self.asgi_client.get("/") assert response.status_code == 200 assert not response.is_redirect + @mock.patch("app.services.location.jhu.datetime") async def test_v1_confirmed(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat + self.mock_client_session.get = mocked_session_get + state = "confirmed" expected_json_output = self.read_file_v1(state=state) - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await self.asgi_client.get("/{}".format(state)) - return_data = response.json() + response = await self.asgi_client.get("/{}".format(state)) + return_data = response.json() assert return_data == json.loads(expected_json_output) + @mock.patch("app.services.location.jhu.datetime") async def test_v1_deaths(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat + self.mock_client_session.get = mocked_session_get + state = "deaths" expected_json_output = self.read_file_v1(state=state) - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await self.asgi_client.get("/{}".format(state)) - return_data = response.json() + response = await self.asgi_client.get("/{}".format(state)) + return_data = response.json() assert return_data == json.loads(expected_json_output) + @mock.patch("app.services.location.jhu.datetime") async def test_v1_recovered(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat + self.mock_client_session.get = mocked_session_get + state = "recovered" expected_json_output = self.read_file_v1(state=state) - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await self.asgi_client.get("/{}".format(state)) - return_data = response.json() + response = await self.asgi_client.get("/{}".format(state)) + return_data = response.json() assert return_data == json.loads(expected_json_output) + @mock.patch("app.services.location.jhu.datetime") async def test_v1_all(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat + self.mock_client_session.get = mocked_session_get + state = "all" expected_json_output = self.read_file_v1(state=state) - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await self.asgi_client.get("/{}".format(state)) - return_data = response.json() + response = await self.asgi_client.get("/{}".format(state)) + return_data = response.json() assert return_data == json.loads(expected_json_output) + @mock.patch("app.services.location.jhu.datetime") async def test_v2_latest(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat + self.mock_client_session.get = mocked_session_get + state = "latest" - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await self.asgi_client.get(f"/v2/{state}") - return_data = response.json() + response = await self.asgi_client.get(f"/v2/{state}") + return_data = response.json() check_dict = {"latest": {"confirmed": 1940, "deaths": 1940, "recovered": 0}} assert return_data == check_dict + @mock.patch("app.services.location.jhu.datetime") async def test_v2_locations(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat + self.mock_client_session.get = mocked_session_get + state = "locations" - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await self.asgi_client.get("/v2/{}".format(state)) - return_data = response.json() + response = await self.asgi_client.get("/v2/{}".format(state)) + return_data = response.json() filepath = "tests/expected_output/v2_{state}.json".format(state=state) with open(filepath, "r") as file: @@ -118,16 +124,16 @@ async def test_v2_locations(self, mock_datetime): # TODO: Why is this failing? # assert return_data == json.loads(expected_json_output) + @mock.patch("app.services.location.jhu.datetime") async def test_v2_locations_id(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat + self.mock_client_session.get = mocked_session_get state = "locations" test_id = 1 - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await self.asgi_client.get("/v2/{}/{}".format(state, test_id)) - return_data = response.json() + response = await self.asgi_client.get("/v2/{}/{}".format(state, test_id)) + return_data = response.json() filepath = "tests/expected_output/v2_{state}_id_{test_id}.json".format(state=state, test_id=test_id) with open(filepath, "r") as file: @@ -151,10 +157,9 @@ async def test_v2_locations_id(self, mock_datetime): ({"source": "jhu", "country_code": "US"}, 404), ], ) -async def test_locations_status_code(async_api_client, query_params, expected_status): - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await async_api_client.get("/v2/locations", query_string=query_params) +async def test_locations_status_code(async_api_client, query_params, expected_status, mock_client_session): + mock_client_session.get = mocked_session_get + response = await async_api_client.get("/v2/locations", query_string=query_params) print(f"GET {response.url}\n{response}") print(f"\tjson:\n{pf(response.json())[:1000]}\n\t...") @@ -173,10 +178,9 @@ async def test_locations_status_code(async_api_client, query_params, expected_st {"source": "jhu", "timelines": True}, ], ) -async def test_latest(async_api_client, query_params): - async with mock_client_session() as mocked_client_session: - mocked_client_session.get = mocked_session_get - response = await async_api_client.get("/v2/latest", query_string=query_params) +async def test_latest(async_api_client, query_params, mock_client_session): + mock_client_session.get = mocked_session_get + response = await async_api_client.get("/v2/latest", query_string=query_params) print(f"GET {response.url}\n{response}") From 990c82c866fc70bc343762031ca7658f10d977ed Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 12:42:17 +0200 Subject: [PATCH 28/38] Use mocked_session_get by default One can still define a separate responder function here, but since this is the only response defined for the tests thus far, this seems like a reasonable default. --- tests/conftest.py | 2 ++ tests/test_csbs.py | 2 -- tests/test_jhu.py | 2 -- tests/test_routes.py | 12 ------------ 4 files changed, 2 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9fde87d9..3cab02f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -81,6 +81,7 @@ def mock_client_session_class(request): """ httputils.client_session = request.cls.mock_client_session = mock.AsyncMock() + httputils.client_session.get = mocked_session_get try: yield finally: @@ -94,6 +95,7 @@ async def mock_client_session(): """ httputils.client_session = mock.AsyncMock() + httputils.client_session.get = mocked_session_get try: yield httputils.client_session finally: diff --git a/tests/test_csbs.py b/tests/test_csbs.py index 912bdd11..828a5b65 100644 --- a/tests/test_csbs.py +++ b/tests/test_csbs.py @@ -1,7 +1,6 @@ import pytest from app.services.location import csbs -from tests.conftest import mocked_session_get def mocked_csbs_requests_get(*args, **kwargs): @@ -28,7 +27,6 @@ def read_file(self): @pytest.mark.asyncio async def test_get_locations(mock_client_session): - mock_client_session.get = mocked_session_get data = await csbs.get_locations() assert isinstance(data, list) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 12a3f704..1244d6dd 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -4,7 +4,6 @@ from app import location from app.services.location import jhu -from tests.conftest import mocked_session_get from tests.conftest import mocked_strptime_isoformat DATETIME_STRING = "2020-03-17T10:23:22.505550" @@ -16,7 +15,6 @@ async def test_get_locations(mock_datetime, mock_client_session): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat - mock_client_session.get = mocked_session_get output = await jhu.get_locations() assert isinstance(output, list) diff --git a/tests/test_routes.py b/tests/test_routes.py index 0f38c353..d88372b2 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -6,7 +6,6 @@ import pytest from async_asgi_testclient import TestClient -from .conftest import mocked_session_get from .conftest import mocked_strptime_isoformat from .test_jhu import DATETIME_STRING from app.main import APP @@ -34,8 +33,6 @@ def read_file_v1(self, state): @mock.patch("app.services.location.jhu.datetime") async def test_root_api(self, mock_datetime): """Validate that / returns a 200 and is not a redirect.""" - self.mock_client_session.get = mocked_session_get - response = await self.asgi_client.get("/") assert response.status_code == 200 @@ -45,7 +42,6 @@ async def test_root_api(self, mock_datetime): async def test_v1_confirmed(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat - self.mock_client_session.get = mocked_session_get state = "confirmed" expected_json_output = self.read_file_v1(state=state) @@ -58,7 +54,6 @@ async def test_v1_confirmed(self, mock_datetime): async def test_v1_deaths(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat - self.mock_client_session.get = mocked_session_get state = "deaths" expected_json_output = self.read_file_v1(state=state) @@ -71,7 +66,6 @@ async def test_v1_deaths(self, mock_datetime): async def test_v1_recovered(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat - self.mock_client_session.get = mocked_session_get state = "recovered" expected_json_output = self.read_file_v1(state=state) @@ -84,7 +78,6 @@ async def test_v1_recovered(self, mock_datetime): async def test_v1_all(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = self.date mock_datetime.strptime.side_effect = mocked_strptime_isoformat - self.mock_client_session.get = mocked_session_get state = "all" expected_json_output = self.read_file_v1(state=state) @@ -97,7 +90,6 @@ async def test_v1_all(self, mock_datetime): async def test_v2_latest(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat - self.mock_client_session.get = mocked_session_get state = "latest" response = await self.asgi_client.get(f"/v2/{state}") @@ -111,7 +103,6 @@ async def test_v2_latest(self, mock_datetime): async def test_v2_locations(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat - self.mock_client_session.get = mocked_session_get state = "locations" response = await self.asgi_client.get("/v2/{}".format(state)) @@ -128,7 +119,6 @@ async def test_v2_locations(self, mock_datetime): async def test_v2_locations_id(self, mock_datetime): mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING mock_datetime.strptime.side_effect = mocked_strptime_isoformat - self.mock_client_session.get = mocked_session_get state = "locations" test_id = 1 @@ -158,7 +148,6 @@ async def test_v2_locations_id(self, mock_datetime): ], ) async def test_locations_status_code(async_api_client, query_params, expected_status, mock_client_session): - mock_client_session.get = mocked_session_get response = await async_api_client.get("/v2/locations", query_string=query_params) print(f"GET {response.url}\n{response}") @@ -179,7 +168,6 @@ async def test_locations_status_code(async_api_client, query_params, expected_st ], ) async def test_latest(async_api_client, query_params, mock_client_session): - mock_client_session.get = mocked_session_get response = await async_api_client.get("/v2/latest", query_string=query_params) print(f"GET {response.url}\n{response}") From 6f231ce30c569ea2b0ed1f75304a5483329caae5 Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 12:58:16 +0200 Subject: [PATCH 29/38] Add test for ClientSession setup/teardown --- tests/test_httputils.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_httputils.py b/tests/test_httputils.py index e69de29b..b5742afa 100644 --- a/tests/test_httputils.py +++ b/tests/test_httputils.py @@ -0,0 +1,19 @@ +import pytest + +from app.utils import httputils + + +@pytest.mark.asyncio +async def test_setup_teardown_client_session(): + with pytest.raises(AttributeError): + # Ensure client_session is undefined prior to setup + httputils.client_session + + await httputils.setup_client_session() + + assert httputils.client_session + + await httputils.teardown_client_session() + assert httputils.client_session.closed + + del httputils.client_session From b25fe73dce75b575bc87fe31815580d0c76af9a4 Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 13:27:58 +0200 Subject: [PATCH 30/38] Update Pipfile.lock --- Pipfile.lock | 128 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 4 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 6d4da039..500275a9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6135cc5a7f50377967629ba129a9747170d933706e42b8b74e29a5d4713aa4e0" + "sha256": "71ab11be8ac956d1b6ebb10f6bbd7496331b749cfaf1f540536321c5ad328e40" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,45 @@ ] }, "default": { + "aiohttp": { + "hashes": [ + "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", + "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", + "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", + "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", + "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", + "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", + "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", + "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", + "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", + "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", + "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", + "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + ], + "index": "pypi", + "version": "==3.6.2" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "asyncache": { + "hashes": [ + "sha256:c741b3ccef2c5291b3da05d97bab3cc8d50f2ac8efd7fd79d47e3d7b6a3774de" + ], + "index": "pypi", + "version": "==0.1.1" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, "cachetools": { "hashes": [ "sha256:9a52dd97a85f257f4e4127f15818e71a0c7899f121b34591fcc1173ea79a0198", @@ -47,11 +86,11 @@ }, "fastapi": { "hashes": [ - "sha256:c2d572370153a6b74d62a73252d75934e2bfdbb0f620fecfd489b5d4789f5c48", - "sha256:c478bc513d192f6776fd3f0355b7ff5414e94ed842677294c06e348105aaa237" + "sha256:a5cb9100d5f2b5dd82addbc2cdf8009258bce45b03ba21d3f5eecc88c7b5a716", + "sha256:cf26d47ede6bc6e179df951312f55fea7d4005dd53370245e216436ca4e22f22" ], "index": "pypi", - "version": "==0.53.1" + "version": "==0.53.2" }, "gunicorn": { "hashes": [ @@ -93,6 +132,28 @@ ], "version": "==2.9" }, + "multidict": { + "hashes": [ + "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", + "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", + "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", + "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", + "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", + "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", + "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", + "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", + "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", + "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", + "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", + "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", + "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", + "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", + "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", + "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", + "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" + ], + "version": "==4.7.5" + }, "pydantic": { "hashes": [ "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752", @@ -206,6 +267,28 @@ "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" ], "version": "==8.1" + }, + "yarl": { + "hashes": [ + "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", + "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", + "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", + "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", + "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", + "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", + "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", + "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", + "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", + "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", + "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", + "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", + "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", + "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", + "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", + "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", + "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" + ], + "version": "==1.4.2" } }, "develop": { @@ -223,6 +306,13 @@ ], "version": "==2.3.3" }, + "async-asgi-testclient": { + "hashes": [ + "sha256:e961c61123eca6dc30c4f67df7fe8a3f695ca9c8b013d97272b930d6d5af4509" + ], + "index": "pypi", + "version": "==1.4.4" + }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", @@ -395,6 +485,28 @@ ], "version": "==8.2.0" }, + "multidict": { + "hashes": [ + "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", + "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", + "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", + "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", + "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", + "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", + "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", + "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", + "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", + "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", + "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", + "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", + "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", + "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", + "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", + "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", + "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" + ], + "version": "==4.7.5" + }, "packaging": { "hashes": [ "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", @@ -453,6 +565,14 @@ "index": "pypi", "version": "==5.4.1" }, + "pytest-asyncio": { + "hashes": [ + "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf", + "sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b" + ], + "index": "pypi", + "version": "==0.10.0" + }, "pytest-cov": { "hashes": [ "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", From 27d30fb4d58f6da531c66759a3c3469ccf64665d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 2 Apr 2020 07:30:44 -0400 Subject: [PATCH 31/38] add LGTM alerts badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index df66078d..8fb2e962 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Support multiple data-sources. [![GitHub last commit](https://img.shields.io/github/last-commit/ExpDev07/coronavirus-tracker-api)](https://github.com/ExpDev07/coronavirus-tracker-api/commits/master) [![GitHub pull requests](https://img.shields.io/github/issues-pr/ExpDev07/coronavirus-tracker-api)](https://github.com/ExpDev07/coronavirus-tracker-api/pulls) [![GitHub issues](https://img.shields.io/github/issues/ExpDev07/coronavirus-tracker-api)](https://github.com/ExpDev07/coronavirus-tracker-api/issues) +[![Total alerts](https://img.shields.io/lgtm/alerts/g/ExpDev07/coronavirus-tracker-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ExpDev07/coronavirus-tracker-api/alerts/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Tweet](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2FExpDev07%2Fcoronavirus-tracker-api)](https://twitter.com/intent/tweet?text=COVID19%20Live%20Tracking%20API:%20&url=https%3A%2F%2Fgithub.com%2FExpDev07%2Fcoronavirus-tracker-api) From 77100b0d9039b3baeaabac5394ca755f34bdaa81 Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 14:42:27 +0200 Subject: [PATCH 32/38] Fix linter warnings --- app/router/v1/confirmed.py | 4 ++-- app/router/v1/deaths.py | 4 ++-- app/router/v1/recovered.py | 4 ++-- app/services/location/csbs.py | 2 +- app/services/location/jhu.py | 2 +- app/utils/httputils.py | 11 ++++++----- tests/conftest.py | 14 +++++++------- tests/test_httputils.py | 8 ++++---- 8 files changed, 25 insertions(+), 24 deletions(-) diff --git a/app/router/v1/confirmed.py b/app/router/v1/confirmed.py index eda7702e..13365e32 100644 --- a/app/router/v1/confirmed.py +++ b/app/router/v1/confirmed.py @@ -6,6 +6,6 @@ @V1.get("/confirmed") async def confirmed(): """Confirmed cases.""" - confirmed = await get_category("confirmed") + confirmed_data = await get_category("confirmed") - return confirmed + return confirmed_data diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py index d41d1d9d..fb45498c 100644 --- a/app/router/v1/deaths.py +++ b/app/router/v1/deaths.py @@ -6,6 +6,6 @@ @V1.get("/deaths") async def deaths(): """Total deaths.""" - deaths = await get_category("deaths") + deaths_data = await get_category("deaths") - return deaths + return deaths_data diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py index 24fcb4fd..3a3a85b7 100644 --- a/app/router/v1/recovered.py +++ b/app/router/v1/recovered.py @@ -6,6 +6,6 @@ @V1.get("/recovered") async def recovered(): """Recovered cases.""" - recovered = await get_category("recovered") + recovered_data = await get_category("recovered") - return recovered + return recovered_data diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index 95f1c4b2..dbd8d82d 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -39,7 +39,7 @@ async def get_locations(): :returns: The locations. :rtype: dict """ - async with httputils.client_session.get(BASE_URL) as response: + async with httputils.CLIENT_SESSION.get(BASE_URL) as response: text = await response.text() data = list(csv.DictReader(text.splitlines())) diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index adee6bdc..316de367 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -55,7 +55,7 @@ async def get_category(category): url = BASE_URL + "time_series_covid19_%s_global.csv" % category # Request the data - async with httputils.client_session.get(url) as response: + async with httputils.CLIENT_SESSION.get(url) as response: text = await response.text() # Parse the CSV. diff --git a/app/utils/httputils.py b/app/utils/httputils.py index de5d5b17..191bba87 100644 --- a/app/utils/httputils.py +++ b/app/utils/httputils.py @@ -1,10 +1,11 @@ +"""app.utils.httputils.py""" import logging from aiohttp import ClientSession # Singleton aiohttp.ClientSession instance. -client_session: ClientSession +CLIENT_SESSION: ClientSession LOGGER = logging.getLogger(__name__) @@ -17,14 +18,14 @@ async def setup_client_session(): See: https://docs.aiohttp.org/en/stable/client_quickstart.html#make-a-request """ - global client_session + global CLIENT_SESSION # pylint: disable=global-statement LOGGER.info("Setting up global aiohttp.ClientSession.") - client_session = ClientSession() + CLIENT_SESSION = ClientSession() async def teardown_client_session(): """Close the application-global aiohttp.ClientSession. """ - global client_session + global CLIENT_SESSION # pylint: disable=global-statement LOGGER.info("Closing global aiohttp.ClientSession.") - await client_session.close() + await CLIENT_SESSION.close() diff --git a/tests/conftest.py b/tests/conftest.py index 3cab02f0..fe58a6b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,12 +80,12 @@ def mock_client_session_class(request): See: https://docs.pytest.org/en/5.4.1/unittest.html#mixing-pytest-fixtures-into-unittest-testcase-subclasses-using-marks """ - httputils.client_session = request.cls.mock_client_session = mock.AsyncMock() - httputils.client_session.get = mocked_session_get + httputils.CLIENT_SESSION = request.cls.mock_client_session = mock.AsyncMock() + httputils.CLIENT_SESSION.get = mocked_session_get try: yield finally: - del httputils.client_session + del httputils.CLIENT_SESSION @pytest.fixture @@ -94,12 +94,12 @@ async def mock_client_session(): instance. """ - httputils.client_session = mock.AsyncMock() - httputils.client_session.get = mocked_session_get + httputils.CLIENT_SESSION = mock.AsyncMock() + httputils.CLIENT_SESSION.get = mocked_session_get try: - yield httputils.client_session + yield httputils.CLIENT_SESSION finally: - del httputils.client_session + del httputils.CLIENT_SESSION @asynccontextmanager diff --git a/tests/test_httputils.py b/tests/test_httputils.py index b5742afa..547f3725 100644 --- a/tests/test_httputils.py +++ b/tests/test_httputils.py @@ -7,13 +7,13 @@ async def test_setup_teardown_client_session(): with pytest.raises(AttributeError): # Ensure client_session is undefined prior to setup - httputils.client_session + httputils.CLIENT_SESSION await httputils.setup_client_session() - assert httputils.client_session + assert httputils.CLIENT_SESSION await httputils.teardown_client_session() - assert httputils.client_session.closed + assert httputils.CLIENT_SESSION.closed - del httputils.client_session + del httputils.CLIENT_SESSION From f9b0ce1e24232695132d0a32de3ed6e5962ca61a Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 14:42:41 +0200 Subject: [PATCH 33/38] Black formatting --- app/utils/httputils.py | 1 - tests/test_routes.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/utils/httputils.py b/app/utils/httputils.py index 191bba87..a0793170 100644 --- a/app/utils/httputils.py +++ b/app/utils/httputils.py @@ -3,7 +3,6 @@ from aiohttp import ClientSession - # Singleton aiohttp.ClientSession instance. CLIENT_SESSION: ClientSession diff --git a/tests/test_routes.py b/tests/test_routes.py index d88372b2..540372ea 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -6,9 +6,10 @@ import pytest from async_asgi_testclient import TestClient +from app.main import APP + from .conftest import mocked_strptime_isoformat from .test_jhu import DATETIME_STRING -from app.main import APP @pytest.mark.usefixtures("mock_client_session_class") From 99ea07e8a5fba816fe8afa1d2e4c21b94d4f067b Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 15:30:55 +0200 Subject: [PATCH 34/38] Fallback to pypi-provided backports for async test utils --- Pipfile | 2 ++ Pipfile.lock | 25 ++++++++++++++++++++++++- app/services/location/csbs.py | 1 + app/services/location/jhu.py | 1 + tests/conftest.py | 22 +++++++++++++++++----- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/Pipfile b/Pipfile index d5097194..be7444e3 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,8 @@ verify_ssl = true [dev-packages] async-asgi-testclient = "*" +async_generator = "*" +asyncmock = "*" bandit = "*" black = "==19.10b0" coveralls = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 500275a9..8b71e7cd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "71ab11be8ac956d1b6ebb10f6bbd7496331b749cfaf1f540536321c5ad328e40" + "sha256": "72d35102ae55e8201c5f3f950096e40b4ff279fcb9c5a009f1e25f8778f83808" }, "pipfile-spec": 6, "requires": { @@ -313,6 +313,22 @@ "index": "pypi", "version": "==1.4.4" }, + "async-generator": { + "hashes": [ + "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", + "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" + ], + "index": "pypi", + "version": "==1.10" + }, + "asyncmock": { + "hashes": [ + "sha256:c251889d542e98fe5f7ece2b5b8643b7d62b50a5657d34a4cbce8a1d5170d750", + "sha256:fd8bc4e7813251a8959d1140924ccba3adbbc7af885dba7047c67f73c0b664b1" + ], + "index": "pypi", + "version": "==0.4.2" + }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", @@ -478,6 +494,13 @@ ], "version": "==0.6.1" }, + "mock": { + "hashes": [ + "sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0", + "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72" + ], + "version": "==4.0.2" + }, "more-itertools": { "hashes": [ "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index dbd8d82d..8487c387 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -3,6 +3,7 @@ from datetime import datetime from asyncache import cached + from cachetools import TTLCache from ...coordinates import Coordinates diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index 316de367..269292ab 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -3,6 +3,7 @@ from datetime import datetime from asyncache import cached + from cachetools import TTLCache from ...coordinates import Coordinates diff --git a/tests/conftest.py b/tests/conftest.py index fe58a6b9..99ce7b96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,15 +5,27 @@ """ import datetime import os -from contextlib import asynccontextmanager -from unittest import mock import pytest from async_asgi_testclient import TestClient as AsyncTestClient -from fastapi.testclient import TestClient from app.main import APP from app.utils import httputils +from fastapi.testclient import TestClient + +try: + from unittest.mock import AsyncMock +except ImportError: + # Python 3.7 backwards compat + from asyncmock import AsyncMock + +try: + from contextlib import asynccontextmanager +except ImportError: + # Python 3.6 backwards compat + from async_generator import asynccontextmanager + + @pytest.fixture @@ -80,7 +92,7 @@ def mock_client_session_class(request): See: https://docs.pytest.org/en/5.4.1/unittest.html#mixing-pytest-fixtures-into-unittest-testcase-subclasses-using-marks """ - httputils.CLIENT_SESSION = request.cls.mock_client_session = mock.AsyncMock() + httputils.CLIENT_SESSION = request.cls.mock_client_session = AsyncMock() httputils.CLIENT_SESSION.get = mocked_session_get try: yield @@ -94,7 +106,7 @@ async def mock_client_session(): instance. """ - httputils.CLIENT_SESSION = mock.AsyncMock() + httputils.CLIENT_SESSION = AsyncMock() httputils.CLIENT_SESSION.get = mocked_session_get try: yield httputils.CLIENT_SESSION From 71e2190021b680d4e80bac29ae963348ee44b2d2 Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 16:11:44 +0200 Subject: [PATCH 35/38] Avoid mock.patch decorators Using these as decorators causes a lot of problems when combined with Pytest fixtures, often resulting in the mock not applying. --- tests/test_jhu.py | 11 +++-- tests/test_routes.py | 100 +++++++++++++++++++++---------------------- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/tests/test_jhu.py b/tests/test_jhu.py index 1244d6dd..3790218d 100644 --- a/tests/test_jhu.py +++ b/tests/test_jhu.py @@ -10,12 +10,11 @@ @pytest.mark.asyncio -@mock.patch("app.services.location.jhu.datetime") -async def test_get_locations(mock_datetime, mock_client_session): - mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING - mock_datetime.strptime.side_effect = mocked_strptime_isoformat - - output = await jhu.get_locations() +async def test_get_locations(mock_client_session): + with mock.patch("app.services.location.jhu.datetime") as mock_datetime: + mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING + mock_datetime.strptime.side_effect = mocked_strptime_isoformat + output = await jhu.get_locations() assert isinstance(output, list) assert isinstance(output[0], location.Location) diff --git a/tests/test_routes.py b/tests/test_routes.py index 540372ea..605ce2c0 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -31,82 +31,81 @@ def read_file_v1(self, state): expected_json_output = file.read() return expected_json_output - @mock.patch("app.services.location.jhu.datetime") - async def test_root_api(self, mock_datetime): + async def test_root_api(self): """Validate that / returns a 200 and is not a redirect.""" response = await self.asgi_client.get("/") assert response.status_code == 200 assert not response.is_redirect - @mock.patch("app.services.location.jhu.datetime") - async def test_v1_confirmed(self, mock_datetime): - mock_datetime.utcnow.return_value.isoformat.return_value = self.date - mock_datetime.strptime.side_effect = mocked_strptime_isoformat - + async def test_v1_confirmed(self): state = "confirmed" expected_json_output = self.read_file_v1(state=state) - response = await self.asgi_client.get("/{}".format(state)) - return_data = response.json() - assert return_data == json.loads(expected_json_output) + with mock.patch("app.services.location.jhu.datetime") as mock_datetime: + mock_datetime.utcnow.return_value.isoformat.return_value = self.date + mock_datetime.strptime.side_effect = mocked_strptime_isoformat + response = await self.asgi_client.get("/{}".format(state)) - @mock.patch("app.services.location.jhu.datetime") - async def test_v1_deaths(self, mock_datetime): - mock_datetime.utcnow.return_value.isoformat.return_value = self.date - mock_datetime.strptime.side_effect = mocked_strptime_isoformat + return_data = response.json() + assert return_data == json.loads(expected_json_output) + async def test_v1_deaths(self): state = "deaths" expected_json_output = self.read_file_v1(state=state) - response = await self.asgi_client.get("/{}".format(state)) - return_data = response.json() - assert return_data == json.loads(expected_json_output) + with mock.patch("app.services.location.jhu.datetime") as mock_datetime: + mock_datetime.utcnow.return_value.isoformat.return_value = self.date + mock_datetime.strptime.side_effect = mocked_strptime_isoformat + response = await self.asgi_client.get("/{}".format(state)) - @mock.patch("app.services.location.jhu.datetime") - async def test_v1_recovered(self, mock_datetime): - mock_datetime.utcnow.return_value.isoformat.return_value = self.date - mock_datetime.strptime.side_effect = mocked_strptime_isoformat + return_data = response.json() + assert return_data == json.loads(expected_json_output) + async def test_v1_recovered(self): state = "recovered" expected_json_output = self.read_file_v1(state=state) - response = await self.asgi_client.get("/{}".format(state)) - return_data = response.json() - assert return_data == json.loads(expected_json_output) + with mock.patch("app.services.location.jhu.datetime") as mock_datetime: + mock_datetime.utcnow.return_value.isoformat.return_value = self.date + mock_datetime.strptime.side_effect = mocked_strptime_isoformat + response = await self.asgi_client.get("/{}".format(state)) - @mock.patch("app.services.location.jhu.datetime") - async def test_v1_all(self, mock_datetime): - mock_datetime.utcnow.return_value.isoformat.return_value = self.date - mock_datetime.strptime.side_effect = mocked_strptime_isoformat + return_data = response.json() + assert return_data == json.loads(expected_json_output) + async def test_v1_all(self): state = "all" expected_json_output = self.read_file_v1(state=state) - response = await self.asgi_client.get("/{}".format(state)) - return_data = response.json() - assert return_data == json.loads(expected_json_output) + with mock.patch("app.services.location.jhu.datetime") as mock_datetime: + mock_datetime.utcnow.return_value.isoformat.return_value = self.date + mock_datetime.strptime.side_effect = mocked_strptime_isoformat + response = await self.asgi_client.get("/{}".format(state)) - @mock.patch("app.services.location.jhu.datetime") - async def test_v2_latest(self, mock_datetime): - mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING - mock_datetime.strptime.side_effect = mocked_strptime_isoformat + return_data = response.json() + assert return_data == json.loads(expected_json_output) + async def test_v2_latest(self): state = "latest" - response = await self.asgi_client.get(f"/v2/{state}") - return_data = response.json() - check_dict = {"latest": {"confirmed": 1940, "deaths": 1940, "recovered": 0}} + with mock.patch("app.services.location.jhu.datetime") as mock_datetime: + mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING + mock_datetime.strptime.side_effect = mocked_strptime_isoformat + response = await self.asgi_client.get(f"/v2/{state}") + return_data = response.json() + check_dict = {"latest": {"confirmed": 1940, "deaths": 1940, "recovered": 0}} assert return_data == check_dict - @mock.patch("app.services.location.jhu.datetime") - async def test_v2_locations(self, mock_datetime): - mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING - mock_datetime.strptime.side_effect = mocked_strptime_isoformat - + async def test_v2_locations(self): state = "locations" - response = await self.asgi_client.get("/v2/{}".format(state)) + + with mock.patch("app.services.location.jhu.datetime") as mock_datetime: + mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING + mock_datetime.strptime.side_effect = mocked_strptime_isoformat + response = await self.asgi_client.get("/v2/{}".format(state)) + return_data = response.json() filepath = "tests/expected_output/v2_{state}.json".format(state=state) @@ -116,14 +115,15 @@ async def test_v2_locations(self, mock_datetime): # TODO: Why is this failing? # assert return_data == json.loads(expected_json_output) - @mock.patch("app.services.location.jhu.datetime") - async def test_v2_locations_id(self, mock_datetime): - mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING - mock_datetime.strptime.side_effect = mocked_strptime_isoformat - + async def test_v2_locations_id(self): state = "locations" test_id = 1 - response = await self.asgi_client.get("/v2/{}/{}".format(state, test_id)) + + with mock.patch("app.services.location.jhu.datetime") as mock_datetime: + mock_datetime.utcnow.return_value.isoformat.return_value = DATETIME_STRING + mock_datetime.strptime.side_effect = mocked_strptime_isoformat + response = await self.asgi_client.get("/v2/{}/{}".format(state, test_id)) + return_data = response.json() filepath = "tests/expected_output/v2_{state}_id_{test_id}.json".format(state=state, test_id=test_id) From 90394da019aa162d6f82feac477ab41ea4788447 Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 16:12:34 +0200 Subject: [PATCH 36/38] Run formatter --- app/services/location/csbs.py | 1 - app/services/location/jhu.py | 1 - tests/conftest.py | 4 +--- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index 8487c387..dbd8d82d 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -3,7 +3,6 @@ from datetime import datetime from asyncache import cached - from cachetools import TTLCache from ...coordinates import Coordinates diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index 269292ab..316de367 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -3,7 +3,6 @@ from datetime import datetime from asyncache import cached - from cachetools import TTLCache from ...coordinates import Coordinates diff --git a/tests/conftest.py b/tests/conftest.py index 99ce7b96..b6399fec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,10 +8,10 @@ import pytest from async_asgi_testclient import TestClient as AsyncTestClient +from fastapi.testclient import TestClient from app.main import APP from app.utils import httputils -from fastapi.testclient import TestClient try: from unittest.mock import AsyncMock @@ -26,8 +26,6 @@ from async_generator import asynccontextmanager - - @pytest.fixture def api_client(): """ From 4d9b8487db33e82931c7e1a6e4add647f97fbb24 Mon Sep 17 00:00:00 2001 From: james-gray Date: Thu, 2 Apr 2020 16:14:47 +0200 Subject: [PATCH 37/38] Add missing dependency --- Pipfile | 3 +++ Pipfile.lock | 32 +++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index be7444e3..0db88935 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,7 @@ asyncmock = "*" bandit = "*" black = "==19.10b0" coveralls = "*" +importlib-metadata = "*" invoke = "*" isort = "*" pylint = "*" @@ -21,8 +22,10 @@ pytest-cov = "*" aiohttp = "*" asyncache = "*" cachetools = "*" +dataclasses = "*" fastapi = "*" gunicorn = "*" +idna_ssl = "*" python-dateutil = "*" python-dotenv = "*" requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 8b71e7cd..7915caf7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "72d35102ae55e8201c5f3f950096e40b4ff279fcb9c5a009f1e25f8778f83808" + "sha256": "fbd18d0bdfc45ec9e02a2f72483d8c0289f0226e87883ee6da7ed75f4f4800a9" }, "pipfile-spec": 6, "requires": { @@ -84,6 +84,14 @@ ], "version": "==7.1.1" }, + "dataclasses": { + "hashes": [ + "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", + "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" + ], + "index": "pypi", + "version": "==0.6" + }, "fastapi": { "hashes": [ "sha256:a5cb9100d5f2b5dd82addbc2cdf8009258bce45b03ba21d3f5eecc88c7b5a716", @@ -132,6 +140,13 @@ ], "version": "==2.9" }, + "idna-ssl": { + "hashes": [ + "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" + ], + "index": "pypi", + "version": "==1.1.0" + }, "multidict": { "hashes": [ "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", @@ -444,6 +459,14 @@ ], "version": "==2.9" }, + "importlib-metadata": { + "hashes": [ + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + ], + "index": "pypi", + "version": "==1.6.0" + }, "invoke": { "hashes": [ "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132", @@ -727,6 +750,13 @@ "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" ], "version": "==1.11.2" + }, + "zipp": { + "hashes": [ + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + ], + "version": "==3.1.0" } } } From c1880583a3496a88083a85d6d23c56d9611cf053 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 2 Apr 2020 22:00:48 -0400 Subject: [PATCH 38/38] add markers to backports --- Pipfile | 6 +++--- Pipfile.lock | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Pipfile b/Pipfile index 0db88935..b337c22a 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ asyncmock = "*" bandit = "*" black = "==19.10b0" coveralls = "*" -importlib-metadata = "*" +importlib-metadata = {version="*", markers="python_version<'3.8'"} invoke = "*" isort = "*" pylint = "*" @@ -22,10 +22,10 @@ pytest-cov = "*" aiohttp = "*" asyncache = "*" cachetools = "*" -dataclasses = "*" +dataclasses = {version="*", markers="python_version<'3.7'"} fastapi = "*" gunicorn = "*" -idna_ssl = "*" +idna_ssl = {version="*", markers="python_version<'3.7'"} python-dateutil = "*" python-dotenv = "*" requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 7915caf7..a699f880 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fbd18d0bdfc45ec9e02a2f72483d8c0289f0226e87883ee6da7ed75f4f4800a9" + "sha256": "1911b081cecdda482b2a9c7c03ebba985c447846506b607df01563600c23126b" }, "pipfile-spec": 6, "requires": { @@ -90,6 +90,7 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", + "markers": "python_version < '3.7'", "version": "==0.6" }, "fastapi": { @@ -145,6 +146,7 @@ "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" ], "index": "pypi", + "markers": "python_version < '3.7'", "version": "==1.1.0" }, "multidict": { @@ -465,6 +467,7 @@ "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" ], "index": "pypi", + "markers": "python_version < '3.8'", "version": "==1.6.0" }, "invoke": {