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):