diff --git a/app/__init__.py b/app/__init__.py index 57721529..7a7badd2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,5 +3,9 @@ ~~~~~~~~~~~~~~~~~~~~~~~~ API for tracking the global coronavirus (COVID-19, SARS-CoV-2) outbreak. """ +import logging + # See PEP396. __version__ = "2.0.3" + +logging.basicConfig(level=logging.INFO) diff --git a/app/enums/sources.py b/app/enums/sources.py deleted file mode 100644 index 9fc00744..00000000 --- a/app/enums/sources.py +++ /dev/null @@ -1,11 +0,0 @@ -from enum import Enum - - -class Sources(str, Enum): - """ - A source available for retrieving data. - """ - - jhu = "jhu" - csbs = "csbs" - nyt = "nyt" diff --git a/app/main.py b/app/main.py index 1224c24e..0ab95fdb 100644 --- a/app/main.py +++ b/app/main.py @@ -12,8 +12,7 @@ from fastapi.responses import JSONResponse from .data import data_source -from .router.v1 import V1 -from .router.v2 import V2 +from .routers import V1, V2 from .utils.httputils import setup_client_session, teardown_client_session # ############ @@ -61,7 +60,7 @@ async def add_datasource(request: Request, call_next): request.state.source = source # Move on... - LOGGER.info(f"source provided: {source.__class__.__name__}") + LOGGER.debug(f"source provided: {source.__class__.__name__}") response = await call_next(request) return response diff --git a/app/models/location.py b/app/models.py similarity index 56% rename from app/models/location.py rename to app/models.py index 48fa4d74..8875a92c 100644 --- a/app/models/location.py +++ b/app/models.py @@ -1,9 +1,44 @@ +"""app.models.py""" from typing import Dict, List from pydantic import BaseModel -from .latest import Latest -from .timeline import Timelines + +class Latest(BaseModel): + """ + Latest model. + """ + + confirmed: int + deaths: int + recovered: int + + +class LatestResponse(BaseModel): + """ + Response for latest. + """ + + latest: Latest + + +class Timeline(BaseModel): + """ + Timeline model. + """ + + latest: int + timeline: Dict[str, int] = {} + + +class Timelines(BaseModel): + """ + Timelines model. + """ + + confirmed: Timeline + deaths: Timeline + recovered: Timeline class Location(BaseModel): diff --git a/app/models/latest.py b/app/models/latest.py deleted file mode 100644 index 6dcfd517..00000000 --- a/app/models/latest.py +++ /dev/null @@ -1,19 +0,0 @@ -from pydantic import BaseModel - - -class Latest(BaseModel): - """ - Latest model. - """ - - confirmed: int - deaths: int - recovered: int - - -class LatestResponse(BaseModel): - """ - Response for latest. - """ - - latest: Latest diff --git a/app/models/timeline.py b/app/models/timeline.py deleted file mode 100644 index 453dfb14..00000000 --- a/app/models/timeline.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Dict - -from pydantic import BaseModel - - -class Timeline(BaseModel): - """ - Timeline model. - """ - - latest: int - timeline: Dict[str, int] = {} - - -class Timelines(BaseModel): - """ - Timelines model. - """ - - confirmed: Timeline - deaths: Timeline - recovered: Timeline diff --git a/app/router/__init__.py b/app/router/__init__.py deleted file mode 100644 index 4eda6c21..00000000 --- a/app/router/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -"""app.router""" -from fastapi import APIRouter - -# pylint: disable=redefined-builtin -from .v1 import all, confirmed, deaths, recovered - -# The routes. -from .v2 import latest, sources, locations # isort:skip diff --git a/app/router/v1/__init__.py b/app/router/v1/__init__.py deleted file mode 100644 index 839bd212..00000000 --- a/app/router/v1/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""app.router.v1""" -from fastapi import APIRouter - -V1 = APIRouter() diff --git a/app/router/v1/all.py b/app/router/v1/all.py deleted file mode 100644 index 91b9e826..00000000 --- a/app/router/v1/all.py +++ /dev/null @@ -1,20 +0,0 @@ -"""app.router.v1.all.py""" -from ...services.location.jhu import get_category -from . import V1 - - -@V1.get("/all") -async def all(): # pylint: disable=redefined-builtin - """Get all the categories.""" - confirmed = await get_category("confirmed") - deaths = await get_category("deaths") - recovered = await 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 deleted file mode 100644 index 13365e32..00000000 --- a/app/router/v1/confirmed.py +++ /dev/null @@ -1,11 +0,0 @@ -"""app.router.v1.confirmed.py""" -from ...services.location.jhu import get_category -from . import V1 - - -@V1.get("/confirmed") -async def confirmed(): - """Confirmed cases.""" - confirmed_data = await get_category("confirmed") - - return confirmed_data diff --git a/app/router/v1/deaths.py b/app/router/v1/deaths.py deleted file mode 100644 index fb45498c..00000000 --- a/app/router/v1/deaths.py +++ /dev/null @@ -1,11 +0,0 @@ -"""app.router.v1.deaths.py""" -from ...services.location.jhu import get_category -from . import V1 - - -@V1.get("/deaths") -async def deaths(): - """Total deaths.""" - deaths_data = await get_category("deaths") - - return deaths_data diff --git a/app/router/v1/recovered.py b/app/router/v1/recovered.py deleted file mode 100644 index 3a3a85b7..00000000 --- a/app/router/v1/recovered.py +++ /dev/null @@ -1,11 +0,0 @@ -"""app.router.v1.recovered.py""" -from ...services.location.jhu import get_category -from . import V1 - - -@V1.get("/recovered") -async def recovered(): - """Recovered cases.""" - recovered_data = await get_category("recovered") - - return recovered_data diff --git a/app/router/v2/__init__.py b/app/router/v2/__init__.py deleted file mode 100644 index 62c31905..00000000 --- a/app/router/v2/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""app.router.v2""" -from fastapi import APIRouter - -V2 = APIRouter() diff --git a/app/router/v2/latest.py b/app/router/v2/latest.py deleted file mode 100644 index 105b16fe..00000000 --- a/app/router/v2/latest.py +++ /dev/null @@ -1,21 +0,0 @@ -"""app.router.v2.latest.py""" -from fastapi import Request - -from ...enums.sources import Sources -from ...models.latest import LatestResponse as Latest -from . import V2 - - -@V2.get("/latest", response_model=Latest) -async def get_latest(request: Request, source: Sources = "jhu"): # pylint: disable=unused-argument - """ - Getting latest amount of total confirmed cases, deaths, and recoveries. - """ - locations = await request.state.source.get_all() - return { - "latest": { - "confirmed": sum(map(lambda location: location.confirmed, locations)), - "deaths": sum(map(lambda location: location.deaths, locations)), - "recovered": sum(map(lambda location: location.recovered, locations)), - } - } diff --git a/app/router/v2/sources.py b/app/router/v2/sources.py deleted file mode 100644 index ad906e51..00000000 --- a/app/router/v2/sources.py +++ /dev/null @@ -1,11 +0,0 @@ -"""app.router.v2.sources.py""" -from ...data import DATA_SOURCES -from . import V2 - - -@V2.get("/sources") -async def sources(): - """ - Retrieves a list of data-sources that are availble to use. - """ - return {"sources": list(DATA_SOURCES.keys())} diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 00000000..2bdd5ee3 --- /dev/null +++ b/app/routers/__init__.py @@ -0,0 +1,3 @@ +"""app.routers""" +from .v1 import V1 +from .v2 import V2 diff --git a/app/routers/v1.py b/app/routers/v1.py new file mode 100644 index 00000000..662514a0 --- /dev/null +++ b/app/routers/v1.py @@ -0,0 +1,47 @@ +"""app.routers.v1.py""" +from fastapi import APIRouter + +from ..services.location.jhu import get_category + +V1 = APIRouter() + + +@V1.get("/all") +async def all_categories(): + """Get all the categories.""" + confirmed = await get_category("confirmed") + deaths = await get_category("deaths") + recovered = await get_category("recovered") + + return { + # Data. + "confirmed": confirmed, + "deaths": deaths, + "recovered": recovered, + # Latest. + "latest": {"confirmed": confirmed["latest"], "deaths": deaths["latest"], "recovered": recovered["latest"],}, + } + + +@V1.get("/confirmed") +async def get_confirmed(): + """Confirmed cases.""" + confirmed_data = await get_category("confirmed") + + return confirmed_data + + +@V1.get("/deaths") +async def get_deaths(): + """Total deaths.""" + deaths_data = await get_category("deaths") + + return deaths_data + + +@V1.get("/recovered") +async def get_recovered(): + """Recovered cases.""" + recovered_data = await get_category("recovered") + + return recovered_data diff --git a/app/router/v2/locations.py b/app/routers/v2.py similarity index 59% rename from app/router/v2/locations.py rename to app/routers/v2.py index 649f9c9e..de5a5312 100644 --- a/app/router/v2/locations.py +++ b/app/routers/v2.py @@ -1,14 +1,41 @@ -"""app.router.v2.locations.py""" -from fastapi import HTTPException, Request +"""app.routers.v2""" +import enum -from ...enums.sources import Sources -from ...models.location import LocationResponse as Location -from ...models.location import LocationsResponse as Locations -from . import V2 +from fastapi import APIRouter, HTTPException, Request + +from ..data import DATA_SOURCES +from ..models import LatestResponse, LocationResponse, LocationsResponse + +V2 = APIRouter() + + +class Sources(str, enum.Enum): + """ + A source available for retrieving data. + """ + + jhu = "jhu" + csbs = "csbs" + nyt = "nyt" + + +@V2.get("/latest", response_model=LatestResponse) +async def get_latest(request: Request, source: Sources = "jhu"): # pylint: disable=unused-argument + """ + Getting latest amount of total confirmed cases, deaths, and recoveries. + """ + locations = await request.state.source.get_all() + return { + "latest": { + "confirmed": sum(map(lambda location: location.confirmed, locations)), + "deaths": sum(map(lambda location: location.deaths, locations)), + "recovered": sum(map(lambda location: location.recovered, locations)), + } + } # pylint: disable=unused-argument,too-many-arguments,redefined-builtin -@V2.get("/locations", response_model=Locations, response_model_exclude_unset=True) +@V2.get("/locations", response_model=LocationsResponse, response_model_exclude_unset=True) async def get_locations( request: Request, source: Sources = "jhu", @@ -56,10 +83,18 @@ async def get_locations( # pylint: disable=invalid-name -@V2.get("/locations/{id}", response_model=Location) +@V2.get("/locations/{id}", response_model=LocationResponse) async def get_location_by_id(request: Request, id: int, source: Sources = "jhu", timelines: bool = True): """ Getting specific location by id. """ location = await request.state.source.get(id) return {"location": location.serialize(timelines)} + + +@V2.get("/sources") +async def sources(): + """ + Retrieves a list of data-sources that are availble to use. + """ + return {"sources": list(DATA_SOURCES.keys())} diff --git a/app/services/location/csbs.py b/app/services/location/csbs.py index d0de3837..d660269c 100644 --- a/app/services/location/csbs.py +++ b/app/services/location/csbs.py @@ -11,6 +11,8 @@ from ...utils import httputils from . import LocationService +LOGGER = logging.getLogger("services.location.csbs") + class CSBSLocationService(LocationService): """ @@ -40,15 +42,14 @@ async def get_locations(): :returns: The locations. :rtype: dict """ - logger = logging.getLogger("services.location.csbs") - logger.info("Requesting data...") + LOGGER.info("csbs Requesting data...") async with httputils.CLIENT_SESSION.get(BASE_URL) as response: text = await response.text() - logger.info("Data received") + LOGGER.info("csbs Data received") data = list(csv.DictReader(text.splitlines())) - logger.info("CSV parsed") + LOGGER.info("csbs CSV parsed") locations = [] @@ -83,7 +84,7 @@ async def get_locations(): int(item["Death"] or 0), ) ) - logger.info("Data normalized") + LOGGER.info("csbs Data normalized") # Return the locations. return locations diff --git a/app/services/location/jhu.py b/app/services/location/jhu.py index 39e07ac2..21990c8c 100644 --- a/app/services/location/jhu.py +++ b/app/services/location/jhu.py @@ -14,6 +14,8 @@ from ...utils import httputils from . import LocationService +LOGGER = logging.getLogger("services.location.jhu") + class JhuLocationService(LocationService): """ @@ -48,8 +50,6 @@ async def get_category(category): :returns: The data for category. :rtype: dict """ - logger = logging.getLogger("services.location.jhu") - # Adhere to category naming standard. category = category.lower() @@ -57,15 +57,15 @@ async def get_category(category): url = BASE_URL + "time_series_covid19_%s_global.csv" % category # Request the data - logger.info("Requesting data...") + LOGGER.info("jhu Requesting data...") async with httputils.CLIENT_SESSION.get(url) as response: text = await response.text() - logger.info("Data received") + LOGGER.info("jhu Data received") # Parse the CSV. data = list(csv.DictReader(text.splitlines())) - logger.info("CSV parsed") + LOGGER.info("jhu CSV parsed") # The normalized locations. locations = [] @@ -98,7 +98,7 @@ async def get_category(category): "latest": int(latest or 0), } ) - logger.info("Data normalized") + LOGGER.info("jhu Data normalized") # Latest total. latest = sum(map(lambda location: location["latest"], locations)) diff --git a/app/services/location/nyt.py b/app/services/location/nyt.py index 7f12eb62..a6435166 100644 --- a/app/services/location/nyt.py +++ b/app/services/location/nyt.py @@ -12,6 +12,8 @@ from ...utils import httputils from . import LocationService +LOGGER = logging.getLogger("services.location.nyt") + class NYTLocationService(LocationService): """ @@ -72,18 +74,16 @@ async def get_locations(): :returns: The complete data for US Counties. :rtype: dict """ - logger = logging.getLogger("services.location.nyt") - # Request the data. - logger.info("Requesting data...") + LOGGER.info("nyt Requesting data...") async with httputils.CLIENT_SESSION.get(BASE_URL) as response: text = await response.text() - logger.info("Data received") + LOGGER.info("Data received") # Parse the CSV. data = list(csv.DictReader(text.splitlines())) - logger.info("CSV parsed") + LOGGER.info("nyt CSV parsed") # Group together locations (NYT data ordered by dates not location). grouped_locations = get_grouped_locations_dict(data) @@ -125,6 +125,6 @@ async def get_locations(): }, ) ) - logger.info("Data normalized") + LOGGER.info("nyt Data normalized") return locations diff --git a/app/utils/countries.py b/app/utils/countries.py index 5f926f37..ebb09bb3 100644 --- a/app/utils/countries.py +++ b/app/utils/countries.py @@ -374,6 +374,6 @@ def country_code(value): """ 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}'!") + LOGGER.info(f"No country code found for '{value}'. Using '{code}'!") return code diff --git a/app/utils/populations.py b/app/utils/populations.py index f5235fd4..24f0fa4e 100644 --- a/app/utils/populations.py +++ b/app/utils/populations.py @@ -28,7 +28,7 @@ def fetch_populations(save=False): # Fetch the countries. try: - countries = requests.get(GEONAMES_URL, params={"username": "dperic"}, timeout=1.5).json()["geonames"] + countries = requests.get(GEONAMES_URL, params={"username": "dperic"}, timeout=1.25).json()["geonames"] # Go through all the countries and perform the mapping. for country in countries: mappings.update({country["countryCode"]: int(country["population"]) or None}) @@ -40,6 +40,7 @@ def fetch_populations(save=False): mappings = app.io.load(GEONAMES_BACKUP_PATH) LOGGER.info(f"Using backup data from {GEONAMES_BACKUP_PATH}") # Finally, return the mappings. + LOGGER.info("Fetched populations") return mappings