diff --git a/app/enums/sources.py b/app/enums/sources.py new file mode 100644 index 00000000..2dd7e13b --- /dev/null +++ b/app/enums/sources.py @@ -0,0 +1,8 @@ +from enum import Enum + +class Sources(str, Enum): + """ + A source available for retrieving data. + """ + jhu = 'jhu' + csbs = 'csbs' \ No newline at end of file diff --git a/app/main.py b/app/main.py index 90693e84..f44bd984 100644 --- a/app/main.py +++ b/app/main.py @@ -1,42 +1,34 @@ """ app.main.py """ -import datetime as dt -import enum import logging import os import reprlib -from typing import Dict, List +import datetime as dt -import fastapi import pydantic import uvicorn -from fastapi.middleware.wsgi import WSGIMiddleware -from fastapi.middleware.cors import CORSMiddleware -from . import models -from .core import create_app -from .data import data_source, data_sources +from fastapi import FastAPI +from fastapi import Request, Response -# ################ -# Dependencies -# ################ +from fastapi.responses import JSONResponse +from fastapi.middleware.wsgi import WSGIMiddleware +from fastapi.middleware.cors import CORSMiddleware -class Sources(str, enum.Enum): - """ - A source available for retrieving data. - """ - jhu = 'jhu' - csbs = 'csbs' +from .core import create_app +from .data import data_source +from .models.location import LocationResponse as Location, LocationsResponse as Locations +from .models.latest import LatestResponse as Latest # ############ # FastAPI App # ############ LOGGER = logging.getLogger('api') -APP = fastapi.FastAPI( +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.', version='2.0.1', @@ -59,7 +51,7 @@ class Sources(str, enum.Enum): # TODO this could probably just be a FastAPI dependency. @APP.middleware('http') -async def add_datasource(request: fastapi.Request, call_next): +async def add_datasource(request: Request, call_next): """ Attach the data source to the request.state. """ @@ -68,7 +60,7 @@ async def add_datasource(request: fastapi.Request, call_next): # Abort with 404 if source cannot be found. if not source: - return fastapi.Response('The provided data-source was not found.', status_code=404) + return Response('The provided data-source was not found.', status_code=404) # Attach source to request. request.state.source = source @@ -86,104 +78,22 @@ async def add_datasource(request: fastapi.Request, call_next): @APP.exception_handler(pydantic.error_wrappers.ValidationError) async def handle_validation_error( - request: fastapi.Request, exc: pydantic.error_wrappers.ValidationError + request: Request, exc: pydantic.error_wrappers.ValidationError ): """ Handles validation errors. """ - return fastapi.responses.JSONResponse({'message': exc.errors()}, status_code=422) + return JSONResponse({'message': exc.errors()}, status_code=422) # ################ -# Routes +# Routing # ################ -V2 = fastapi.APIRouter() - - -@V2.get('/latest', response_model=models.LatestResponse) -def get_latest(request: fastapi.Request, source: Sources = 'jhu'): - """ - Getting latest amount of total confirmed cases, deaths, and recoveries. - """ - locations = 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)), - } - } - - -@V2.get( - '/locations', response_model=models.LocationsResponse, response_model_exclude_unset=True -) -def get_locations( - request: fastapi.Request, - source: Sources = 'jhu', - country_code: str = None, - province: str = None, - county: str = None, - timelines: bool = False, -): - """ - Getting the locations. - """ - # All query paramameters. - params = dict(request.query_params) - - # Remove reserved params. - params.pop('source', None) - params.pop('timelines', None) - - # Retrieve all the locations. - locations = request.state.source.get_all() - - # Attempt to filter out locations with properties matching the provided query params. - for key, value in params.items(): - # Clean keys for security purposes. - key = key.lower() - value = value.lower().strip('__') - - # Do filtering. - try: - locations = [location for location in locations if str(getattr(location, key)).lower() == str(value)] - except AttributeError: - pass - - # Return final serialized data. - 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)), - }, - 'locations': [location.serialize(timelines) for location in locations], - } - - -@V2.get('/locations/{id}', response_model=models.LocationResponse) -def get_location_by_id(request: fastapi.Request, id: int, source: Sources = 'jhu', timelines: bool = True): - """ - Getting specific location by id. - """ - return { - 'location': request.state.source.get(id).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()) - } +from .router import router # Include routers. -APP.include_router(V2, prefix='/v2', tags=['v2']) +APP.include_router(router, prefix='/v2', tags=['v2']) # mount the existing Flask app # v1 @ / diff --git a/app/models.py b/app/models.py deleted file mode 100644 index 05dcaa9c..00000000 --- a/app/models.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -app.models.py -~~~~~~~~~~~~~ -Reponse data models. -""" -from pydantic import BaseModel -from typing import Dict, List - -class Latest(BaseModel): - confirmed: int - deaths: int - recovered: int - - -class LatestResponse(BaseModel): - latest: Latest - - -class Timeline(BaseModel): - latest: int - timeline: Dict[str, int] = {} - - -class Timelines(BaseModel): - confirmed: Timeline - deaths: Timeline - recovered: Timeline - - -class Location(BaseModel): - id: int - country: str - country_code: str - county: str = '' - province: str = '' - last_updated: str # TODO use datetime.datetime type. - coordinates: Dict - latest: Latest - timelines: Timelines = {} - - -class LocationsResponse(BaseModel): - latest: Latest - locations: List[Location] = [] - - -class LocationResponse(BaseModel): - location: Location diff --git a/app/models/latest.py b/app/models/latest.py new file mode 100644 index 00000000..90493156 --- /dev/null +++ b/app/models/latest.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + +class Latest(BaseModel): + """ + Latest model. + """ + confirmed: int + deaths: int + recovered: int + +class LatestResponse(BaseModel): + """ + Response for latest. + """ + latest: Latest \ No newline at end of file diff --git a/app/models/location.py b/app/models/location.py new file mode 100644 index 00000000..e796fad8 --- /dev/null +++ b/app/models/location.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel +from typing import Dict, List +from .timeline import Timelines +from .latest import Latest + +class Location(BaseModel): + """ + Location model. + """ + id: int + country: str + country_code: str + county: str = '' + province: str = '' + last_updated: str # TODO use datetime.datetime type. + coordinates: Dict + latest: Latest + timelines: Timelines = {} + +class LocationResponse(BaseModel): + """ + Response for location. + """ + location: Location + +class LocationsResponse(BaseModel): + """ + Response for locations. + """ + latest: Latest + locations: List[Location] = [] \ No newline at end of file diff --git a/app/models/timeline.py b/app/models/timeline.py new file mode 100644 index 00000000..33947493 --- /dev/null +++ b/app/models/timeline.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel +from typing import Dict + +class Timeline(BaseModel): + """ + Timeline model. + """ + + latest: int + timeline: Dict[str, int] = {} + +class Timelines(BaseModel): + """ + Timelines model. + """ + confirmed: Timeline + deaths: Timeline + recovered: Timeline \ No newline at end of file diff --git a/app/router/__init__.py b/app/router/__init__.py new file mode 100644 index 00000000..70ccb65d --- /dev/null +++ b/app/router/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +# Create the router. +router = APIRouter() + +# The routes. +from . import latest, sources, locations \ No newline at end of file diff --git a/app/router/latest.py b/app/router/latest.py new file mode 100644 index 00000000..a3914c5e --- /dev/null +++ b/app/router/latest.py @@ -0,0 +1,18 @@ +from fastapi import Request +from . import router +from ..enums.sources import Sources +from ..models.latest import LatestResponse as Latest + +@router.get('/latest', response_model=Latest) +def get_latest(request: Request, source: Sources = 'jhu'): + """ + Getting latest amount of total confirmed cases, deaths, and recoveries. + """ + locations = 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)), + } + } \ No newline at end of file diff --git a/app/router/locations.py b/app/router/locations.py new file mode 100644 index 00000000..4abb11fd --- /dev/null +++ b/app/router/locations.py @@ -0,0 +1,60 @@ +from fastapi import Request +from . import router +from ..enums.sources import Sources +from ..models.location import LocationResponse as Location, LocationsResponse as Locations + +@router.get( + '/locations', response_model=Locations, response_model_exclude_unset=True +) +def get_locations( + request: Request, + source: Sources = 'jhu', + country_code: str = None, + province: str = None, + county: str = None, + timelines: bool = False, +): + """ + Getting the locations. + """ + # All query paramameters. + params = dict(request.query_params) + + # Remove reserved params. + params.pop('source', None) + params.pop('timelines', None) + + # Retrieve all the locations. + locations = request.state.source.get_all() + + # Attempt to filter out locations with properties matching the provided query params. + for key, value in params.items(): + # Clean keys for security purposes. + key = key.lower() + value = value.lower().strip('__') + + # Do filtering. + try: + locations = [location for location in locations if str(getattr(location, key)).lower() == str(value)] + except AttributeError: + pass + + # Return final serialized data. + 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)), + }, + 'locations': [location.serialize(timelines) for location in locations], + } + + +@router.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. + """ + return { + 'location': request.state.source.get(id).serialize(timelines) + } \ No newline at end of file diff --git a/app/router/sources.py b/app/router/sources.py new file mode 100644 index 00000000..25674587 --- /dev/null +++ b/app/router/sources.py @@ -0,0 +1,11 @@ +from . import router +from ..data import data_sources + +@router.get('/sources') +async def sources(): + """ + Retrieves a list of data-sources that are availble to use. + """ + return { + 'sources': list(data_sources.keys()) + } \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py index ebc9eff5..b61458e0 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -6,8 +6,3 @@ # API version 1. from .v1 import confirmed, deaths, recovered, all - -# Redirect to project page on index. -@app.route('/') -def index(): - return redirect('https://github.com/ExpDev07/coronavirus-tracker-api', 302)