Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ install:
- "pip install pipenv"
- "pipenv install --dev --skip-lock"
script:
- "make test lint"
- "make test lint check-fmt"
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ test:

lint:
pylint $(APP) || true

fmt:
isort -rc --atomic
black .

check-fmt:
isort -rc --check
black . --check --diff
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new fmt and check-fmt commands

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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)
[![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)

**Live global stats (provided by [fight-covid19/bagdes](https://github.com/fight-covid19/bagdes)) from this API:**
Expand Down
2 changes: 1 addition & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# See PEP396.
__version__ = '2.0'
__version__ = "2.0"

from .core import create_app
3 changes: 2 additions & 1 deletion app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

# Load enviroment variables from .env file.
from dotenv import load_dotenv

load_dotenv()

"""
The port to serve the app application on.
"""
PORT = int(os.getenv('PORT', 5000))
PORT = int(os.getenv("PORT", 5000))
7 changes: 2 additions & 5 deletions app/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ def serialize(self):
:returns: The serialized coordinates.
:rtype: dict
"""
return {
'latitude' : self.latitude,
'longitude': self.longitude
}
return {"latitude": self.latitude, "longitude": self.longitude}

def __str__(self):
return 'lat: %s, long: %s' % (self.latitude, self.longitude)
return "lat: %s, long: %s" % (self.latitude, self.longitude)
3 changes: 2 additions & 1 deletion app/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from flask import Flask
from flask_cors import CORS


def create_app():
"""
Construct the core application.
Expand All @@ -10,7 +11,7 @@ def create_app():
CORS(app)

# Set app config from settings.
app.config.from_pyfile('config/settings.py');
app.config.from_pyfile("config/settings.py")

with app.app_context():
# Import routes.
Expand Down
10 changes: 4 additions & 6 deletions app/data/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from ..services.location.jhu import JhuLocationService
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):
"""
Expand All @@ -14,4 +12,4 @@ def data_source(source):
:returns: The service.
:rtype: LocationService
"""
return data_sources.get(source.lower())
return data_sources.get(source.lower())
6 changes: 4 additions & 2 deletions app/enums/sources.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from enum import Enum


class Sources(str, Enum):
"""
A source available for retrieving data.
"""
jhu = 'jhu'
csbs = 'csbs'

jhu = "jhu"
csbs = "csbs"
55 changes: 29 additions & 26 deletions app/location/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from ..coordinates import Coordinates
from ..utils import countrycodes


class Location:
"""
A location in the world affected by the coronavirus.
Expand All @@ -20,7 +21,7 @@ def __init__(self, id, country, province, coordinates, last_updated, confirmed,
self.confirmed = confirmed
self.deaths = deaths
self.recovered = recovered

@property
def country_code(self):
"""
Expand All @@ -37,25 +38,19 @@ def serialize(self):
"""
return {
# General info.
'id' : self.id,
'country' : self.country,
'country_code': self.country_code,
'province' : self.province,

"id": self.id,
"country": self.country,
"country_code": self.country_code,
"province": self.province,
# Coordinates.
'coordinates': self.coordinates.serialize(),

"coordinates": self.coordinates.serialize(),
# Last updated.
'last_updated': self.last_updated,

"last_updated": self.last_updated,
# Latest data (statistics).
'latest': {
'confirmed': self.confirmed,
'deaths' : self.deaths,
'recovered': self.recovered
},
"latest": {"confirmed": self.confirmed, "deaths": self.deaths, "recovered": self.recovered},
}


class TimelinedLocation(Location):
"""
A location with timelines.
Expand All @@ -64,18 +59,21 @@ class TimelinedLocation(Location):
def __init__(self, id, country, province, coordinates, last_updated, timelines):
super().__init__(
# General info.
id, country, province, coordinates, last_updated,

id,
country,
province,
coordinates,
last_updated,
# Statistics (retrieve latest from timelines).
confirmed=timelines.get('confirmed').latest or 0,
deaths=timelines.get('deaths').latest or 0,
recovered=timelines.get('recovered').latest or 0,
confirmed=timelines.get("confirmed").latest or 0,
deaths=timelines.get("deaths").latest or 0,
recovered=timelines.get("recovered").latest or 0,
)

# Set timelines.
self.timelines = timelines

def serialize(self, timelines = False):
def serialize(self, timelines=False):
"""
Serializes the location into a dict.

Expand All @@ -87,10 +85,15 @@ def serialize(self, timelines = False):

# Whether to include the timelines or not.
if timelines:
serialized.update({ 'timelines': {
# Serialize all the timelines.
key: value.serialize() for (key, value) in self.timelines.items()
}})
serialized.update(
{
"timelines": {
# Serialize all the timelines.
key: value.serialize()
for (key, value) in self.timelines.items()
}
}
)

# Return the serialized location.
return serialized
return serialized
24 changes: 14 additions & 10 deletions app/location/csbs.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
from . import Location


class CSBSLocation(Location):
"""
A CSBS (county) location.
"""

def __init__(self, id, state, county, coordinates, last_updated, confirmed, deaths):
super().__init__(
# General info.
id, 'US', state, coordinates, last_updated,

id,
"US",
state,
coordinates,
last_updated,
# Statistics.
confirmed=confirmed,
deaths=deaths,
recovered=0
deaths=deaths,
recovered=0,
)

self.state = state
self.county = county

def serialize(self, timelines=False):
"""
Serializes the location into a dict.
Expand All @@ -28,10 +33,9 @@ def serialize(self, timelines=False):
serialized = super().serialize()

# Update with new fields.
serialized.update({
'state': self.state,
'county': self.county,
})
serialized.update(
{"state": self.state, "county": self.county,}
)

# Return the serialized location.
return serialized
return serialized
67 changes: 27 additions & 40 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,36 @@
"""
app.main.py
"""
import datetime as dt
import logging
import os
import reprlib
import datetime as dt

import pydantic
import uvicorn

from fastapi import FastAPI
from fastapi import Request, Response

from fastapi.responses import JSONResponse

from fastapi.middleware.wsgi import WSGIMiddleware
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
from .data import data_source

from .models.location import LocationResponse as Location, LocationsResponse as Locations
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

# ############
# FastAPI App
# ############
LOGGER = logging.getLogger('api')
LOGGER = logging.getLogger("api")

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',
docs_url='/',
redoc_url='/docs',
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",
docs_url="/",
redoc_url="/docs",
)

# #####################
Expand All @@ -42,31 +39,27 @@

# Enable CORS.
APP.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_origins=['*'],
allow_methods=['*'],
allow_headers=['*'],
CORSMiddleware, allow_credentials=True, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
)

# TODO this could probably just be a FastAPI dependency.
@APP.middleware('http')
@APP.middleware("http")
async def add_datasource(request: Request, call_next):
"""
Attach the data source to the request.state.
"""
# Retrieve the datas ource from query param.
source = data_source(request.query_params.get('source', default='jhu'))
source = data_source(request.query_params.get("source", default="jhu"))

# Abort with 404 if source cannot be found.
if not source:
return 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

# Move on...
LOGGER.info(f'source provided: {source.__class__.__name__}')
LOGGER.info(f"source provided: {source.__class__.__name__}")
response = await call_next(request)
return response

Expand All @@ -77,33 +70,27 @@ 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):
"""
Handles validation errors.
"""
return JSONResponse({'message': exc.errors()}, status_code=422)
return JSONResponse({"message": exc.errors()}, status_code=422)


# ################
# Routing
# ################

from .router import router

# Include routers.
APP.include_router(router, prefix='/v2', tags=['v2'])
APP.include_router(router, prefix="/v2", tags=["v2"])

# mount the existing Flask app
# v1 @ /
APP.mount('/', WSGIMiddleware(create_app()))
APP.mount("/", WSGIMiddleware(create_app()))

# Running of app.
if __name__ == '__main__':
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",
)
Loading