Skip to content

Commit

Permalink
chore: updating master to with new staging route, re-factor of API (#53)
Browse files Browse the repository at this point in the history
* refactor: change Aaron to standard feat: adding basic test
- Changed APP to app according to Flask/FastAPI standard
- Changed config_ to _config. trailing space is for python default conflicts not import
- Adding basic tests for endpoints

* test: added test structures
- 	est_app.py: testing �pp.py app title, description, version
- 	est_config.py: tested ProductionConfig, DevelopmentConfig, config loader
- 	est_endpoints.py: tested root test

* test: added test structures
- 	est_app.py: testing �pp.py app title, description, version
- 	est_config.py: tested ProductionConfig, DevelopmentConfig, config loader
- 	est_endpoints.py: tested root test

* tests: add testing for custom error messages

* chore: updated README

* chore: fixed PR template

* chore: updated README

* chore: updated README

* tests: adds test cases for endpoints
- '/' done
- '/news' only testing for 422 and 405, no validation yet
- '/twitter': tested 422, 404, 405, and random data sample validation
- '/county': tested 404, 405, 422. no data return validation yet
- '/state': tested 405, 422.
- '/country': tested 405, 422.
- '/stats': tested 405, 422.

* style: fix for codefactor.
- 	est_config.py: keeping assert == True/False for code readability
- 	est_endpoints.py: keeping TODO as a reminder to fix endpoints.py

* style: changed config_ to �pp_config after technical discussion

* style: chore:
- added .pylintrc
- fixed all files for pylint
- added .github/workflow/pythonapp.yml
- added pipenv, pylint and pytest

* chore:
- streamlining pythonapp.yml
- triggers pythonapp.yml on all push and pull_request

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml, readme.md

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml

* chore: updated pythonapp.yml

* feat: rerouting root endpoint to postman

* feat: rerouting root endpoint to postman

* feat: added redirect to postman, added test

* fix: added uvloop

* feat: adding coverall

* feat: installed coveralls for coverall.io

* feat: installed coveralls for coverall.io

* feat: installed coveralls for coverall.io

* feat: installed coveralls for coverall.io attempt 6

* feat: installed coveralls for coverall.io attempt 8

* feat: installed coveralls for coverall.io attempt 9

* feat: settingup coverall attempt #11

* feat: coverall badge attempt #12

* feat: coverall added, feat: routing root to redoc

* fix: default config logic

* fix: default config logic

* Update LICENSE

* chore: updating README again  🤧 (#46)

* chore: updating READMEs

* chore: updating READMEs

* chore: updating READMEs (#48)

* fix: post county new_death nan error (#50)

* fix: /post county new_death nan error

* fix: /post county new_death nan error

* feat: test: (#52)

* feat: adding zip route

* feat: zip route #1

* feat: removed uszipcode, added zipcodes

* feat:
- feat: added zip endpoint to return county data given zip code
- test: added tests for the zip endpoint
- feat: modified github actions to trigger on push, and on pr to master/staging

* han: attempt to fix codefactor #1

* feat: zip endpoint
- added custom exception handlers
- mal-formed zip codes now return 422 instead of 404
- changed mal-formed zip codes test cases from 404 to 422

Co-authored-by: leehanchung <[email protected]>
Co-authored-by: Hanchung Lee <[email protected]>
  • Loading branch information
3 people committed Apr 20, 2020
1 parent 32d3bec commit 51ecd4a
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 10 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Build

on:
push:
branches: [ staging, master ]
push

pull_request:
branches: [ staging, master ]

Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mongoengine = "*"
beautifulsoup4 = "*"
lxml = "*"
uvloop = "*"
zipcodes = "*"

[dev-packages]
black = "*"
Expand Down
22 changes: 15 additions & 7 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
from starlette.middleware.cors import CORSMiddleware
import api
import api.endpoints
from api.exception_handlers import data_reading_exception_handler
from api.exception_handlers import data_validation_exception_handler
from api.config import get_logger
from api.config import app_config
from api.config import DataReadingError, DataValidationError



_logger = get_logger(logger_name=__name__)
Expand Down Expand Up @@ -36,6 +40,11 @@ def create_app() -> FastAPI:
allow_headers=["*"],
)

app.add_exception_handler(DataReadingError,
data_reading_exception_handler)
app.add_exception_handler(DataValidationError,
data_validation_exception_handler)

_logger.info("FastAPI instance created")

return app
61 changes: 60 additions & 1 deletion api/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydantic import BaseModel
from starlette.responses import JSONResponse, RedirectResponse
from cachetools import cached, TTLCache
import zipcodes

from api.config import app_config
from api.config import get_logger
Expand All @@ -18,9 +19,9 @@
from api.utils import read_country_data
from api.utils import read_county_stats
from api.utils import read_states
from api.utils import read_county_stats_zip_ny
from api.config import DataReadingError


# Starts the FastAPI Router to be used by the FastAPI app.
router = APIRouter()
_logger = get_logger(logger_name=__name__)
Expand Down Expand Up @@ -414,3 +415,61 @@ async def post_stats(stats: StatsInput) -> JSONResponse:
raise HTTPException(status_code=404,
detail=f"[Error] post /stats API: {ex}")
return json_data

###############################################################################
#
# ZIP Endpoint
#
################################################################################
class ZIPInput(BaseModel):
zip_code: str = "70030"

class ZIPStats(BaseModel):
county_name: str = "St. Charles"
state_name: str = "Louisiana"
confirmed: int
new: int
death: int
new_death: int
fatality_rate: str = "5.5%"
latitude: float
longitude: float
last_update: str = "2020-04-17 19:50 EDT"

class ZIPOutput(BaseModel):
success: bool
message: ZIPStats


@router.post("/zip",
response_model=ZIPOutput,
responses={404: {"model": Message}})
def post_zip(zip_code: ZIPInput) -> JSONResponse:
"""Returns county stats for the zip code input.
"""

try:
zip_info = zipcodes.matching(zip_code.zip_code)[0]
except Exception as ex:
_logger.warning(f"Endpoint: /zip --- POST --- {ex}")
raise HTTPException(status_code=422,
detail=f"[Error] get '/zip' API: {ex}")

try:
county = zip_info['county'].rsplit(' ', 1)[0]
state = zip_info['state']
if state == "NY":
print('NY')
data = read_county_stats_zip_ny(zip_code.zip_code)
else:
print("not NY")
data = read_county_stats(state, county)[0]
json_data = {"success": True, "message": data}
del data
gc.collect()
except Exception as ex:
_logger.warning(f"Endpoint: /zip --- POST --- {ex}")
raise HTTPException(status_code=404,
detail=f"[Error] get '/zip' API: {ex}")

return json_data
25 changes: 25 additions & 0 deletions api/exception_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from starlette.responses import JSONResponse
from fastapi import Request

from api.config import DataReadingError, DataValidationError


async def data_reading_exception_handler(
request: Request,
exc: DataReadingError,
) -> JSONResponse:

return JSONResponse(
status_code=422,
content={"message": f"[ERROR] {request} -- {exc.name}"},
)


async def data_validation_exception_handler(
request: Request,
exc: DataValidationError,
) -> JSONResponse:
return JSONResponse(
status_code=422,
content={"message": f"[ERROR] {request} -- {exc.name}"},
)
119 changes: 119 additions & 0 deletions api/tests/test_endpoints.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# pylint: disable=redefined-outer-name

import time
import json
import random
import pytest
Expand Down Expand Up @@ -290,3 +292,120 @@ def test_other_stats(test_app):

response = test_app.delete("/stats")
assert response.status_code == 405


###############################################################################
#
# Test county data endpoints
#
################################################################################
def test_post_ny_zip(test_app):
"""Test problematic zip codes:
10004 -> Manhattan (New York County), NY
10302 -> Staten Island (Richmond County), NY
10458 -> Bronx, NY
"""
payload = {'zip_code': '10004'}
response = test_app.post("/zip", data=json.dumps(payload))
time.sleep(2)
assert response.status_code == 200
data = response.json()['message']
assert data['state_name'] == "New York"
assert data['county_name'] == "New York"

payload = {"zip_code": "10312"}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 200
data = response.json()['message']
assert data['state_name'] == "New York"
assert data['county_name'] == "Richmond"

payload = {'zip_code': '10458'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 200
data = response.json()['message']
assert data['state_name'] == "New York"
assert data['county_name'] == "Bronx"

def test_post_zip(test_app):
"""Test problematic zip codes:
63163 -> Saint Louis, MO
70030 -> St. Charles, LA
70341 -> Assumption Parish, LA
"""
payload = {'zip_code': '63163'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 200
data = response.json()['message']
assert data['state_name'] == "Missouri"
assert data['county_name'] == "St. Louis"

payload = {'zip_code': '70030'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 200
data = response.json()['message']
assert data['state_name'] == "Louisiana"
assert data['county_name'] == "St. Charles"

payload = {'zip_code': '70341'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 200
data = response.json()['message']
assert data['state_name'] == "Louisiana"
assert data['county_name'] == "Assumption"


def test_post_zip_validation(test_app):
"""Unprocessable entity"""
response = test_app.post("/zip")
assert response.status_code == 422

# TODO: /zip post has no input validation, so still return 200
payload = {'testing': 'validation'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 200

# TODO: /zip post has no input validation, so still return 200
payload = {'testing': 'validation', 'this': 'shouldnt work'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 200


def test_post_zip_not_found(test_app):
"""invalid zip codes"""

payload = {'zip_code': '33333'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 422

payload = {'zip_code': '90000'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 422

payload = {'zip_code': '20000'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 422

payload = {'zip_code': '72200'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 422

payload = {'zip_code': '57400'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 422

payload = {'zip_code': '123456'}
response = test_app.post("/zip", data=json.dumps(payload))
assert response.status_code == 422


def test_other_zip(test_app):
"""Methods not allowed"""
response = test_app.put("/zip")
assert response.status_code == 405

response = test_app.patch("/zip")
assert response.status_code == 405

response = test_app.delete("/zip")
assert response.status_code == 405
1 change: 1 addition & 0 deletions api/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .county_mongo import StateMongo
from .county import read_county_stats
from .state import read_states
from .zip import read_county_stats_zip_ny
Loading

0 comments on commit 51ecd4a

Please sign in to comment.