Skip to content

Commit

Permalink
✅ Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
eightBEC committed Feb 15, 2020
0 parents commit dc84eab
Show file tree
Hide file tree
Showing 41 changed files with 667 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
IS_DEBUG = False
API_KEY = sample_api_key
DEFAULT_MODEL_PATH=./sample_model/lin_reg_california_housing_model.joblib
111 changes: 111 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Hidden files
.DS_store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
htmlcov-py36/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

.vscode/
.pytest-cache/
.pytest_cache/
.empty/

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# dotenv
.env

# virtualenv
.venv
venv/
ENV/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.idea/
11 changes: 11 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
5 changes: 5 additions & 0 deletions MANIFEST
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# file GENERATED by distutils, do NOT edit
setup.cfg
setup.py
fastapi_skeleton/__init__.py
fastapi_skeleton/main.py
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# FastAPI Model Server Skeleton

Serving machine learning models production-ready, fast, easy and secure powered by the great FastAPI by [Sebastián Ramírez]([)](https://github.com/tiangolo).

This repository contains a skeleton app which can be used to speed-up your next machine learning project. The code is fully tested and provides a preconfigured `tox` to quickly expand this sample code.

To experiment and get a feeling on how to use this skeleton, a sample regression model for house price prediction is included in this project. Follow the installation and setup instructions to run the sample model and serve it aso RESTful API.

## Requirements

Python 3.6+

## Installation
Install the required packages in your local environment (ideally virtualenv, conda, etc.).
```bash
pip install -r requirements
```


## Setup
1. Duplicate the `.env.example` file and rename it to `.env`


2. In the `.env` file configure the `API_KEY` entry. The key is used for authenticating our API. <br>
A sample API key can be generated using Python REPL:
```python
import uuid
print(str(uuid.uuid4()))
```

## Run It

1. Start your app with:
```bash
uvicorn fastapi_skeleton.main:app
```

2. Go to [http://localhost:8000/docs](http://localhost:8000/docs).

3. Click `Authorize` and enter the API key as created in the Setup step.
![Authroization](./docs/authorize.png)

4. You can use the sample payload from the `docs/sample_payload.json` file when trying out the house price prediction model using the API.
![Prediction with example payload](./docs/sample_payload.png)

## Run Tests

If you're not using `tox`, please install with:
```bash
pip install tox
```

Run your tests with:
```bash
tox
```

This runs tests and coverage for Python 3.6 and Flake8, Autopep8, Bandit.
Empty file added docs/DOCS.md
Empty file.
Binary file added docs/authorize.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/sample_payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"median_income_in_block": 8.3252,
"median_house_age_in_block": 41,
"average_rooms": 6,
"average_bedrooms": 1,
"population_per_block": 322,
"average_house_occupancy": 2.55,
"block_latitude": 37.88,
"block_longitude": -122.23
}
Binary file added docs/sample_payload.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added fastapi_skeleton/__init__.py
Empty file.
Empty file.
Empty file.
12 changes: 12 additions & 0 deletions fastapi_skeleton/api/routes/heartbeat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

from fastapi import APIRouter

from fastapi_skeleton.models.heartbeat import HearbeatResult

router = APIRouter()


@router.get("/heartbeat", response_model=HearbeatResult, name="heartbeat")
def get_hearbeat() -> HearbeatResult:
heartbeat = HearbeatResult(is_alive=True)
return heartbeat
22 changes: 22 additions & 0 deletions fastapi_skeleton/api/routes/prediction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from fastapi import APIRouter, Depends
from starlette.requests import Request

from fastapi_skeleton.core import security
from fastapi_skeleton.models.payload import HousePredictionPayload
from fastapi_skeleton.models.prediction import HousePredictionResult
from fastapi_skeleton.services.models import HousePriceModel

router = APIRouter()


@router.post("/predict", response_model=HousePredictionResult, name="predict")
def post_predict(
request: Request,
authenticated: bool = Depends(security.validate_request),
block_data: HousePredictionPayload = None
) -> HousePredictionResult:

model: HousePriceModel = request.app.state.model
prediction: HousePredictionResult = model.predict(block_data)

return prediction
10 changes: 10 additions & 0 deletions fastapi_skeleton/api/routes/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@


from fastapi import APIRouter

from fastapi_skeleton.api.routes import heartbeat, prediction

api_router = APIRouter()
api_router.include_router(heartbeat.router, tags=["health"], prefix="/health")
api_router.include_router(prediction.router, tags=[
"prediction"], prefix="/model")
Empty file.
15 changes: 15 additions & 0 deletions fastapi_skeleton/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@


from starlette.config import Config
from starlette.datastructures import Secret

APP_VERSION = "0.0.1"
APP_NAME = "House Price Prediction Example"
API_PREFIX = "/api"

config = Config(".env")

API_KEY: Secret = config("API_KEY", cast=Secret)
IS_DEBUG: bool = config("IS_DEBUG", cast=bool, default=False)

DEFAULT_MODEL_PATH: str = config("DEFAULT_MODEL_PATH")
33 changes: 33 additions & 0 deletions fastapi_skeleton/core/event_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@


from typing import Callable

from fastapi import FastAPI
from loguru import logger

from fastapi_skeleton.core.config import DEFAULT_MODEL_PATH
from fastapi_skeleton.services.models import HousePriceModel


def _startup_model(app: FastAPI) -> None:
model_path = DEFAULT_MODEL_PATH
model_instance = HousePriceModel(model_path)
app.state.model = model_instance


def _shutdown_model(app: FastAPI) -> None:
app.state.model = None


def start_app_handler(app: FastAPI) -> Callable:
def startup() -> None:
logger.info("Running app start handler.")
_startup_model(app)
return startup


def stop_app_handler(app: FastAPI) -> Callable:
def shutdown() -> None:
logger.info("Running app shutdown handler.")
_shutdown_model(app)
return shutdown
6 changes: 6 additions & 0 deletions fastapi_skeleton/core/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NO_API_KEY = "No API key provided."
AUTH_REQ = "Authentication required."
HTTP_500_DETAIL = "Internal server error."

# templates
NO_VALID_PAYLOAD = "{} is not a valid payload."
25 changes: 25 additions & 0 deletions fastapi_skeleton/core/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@


import secrets
from typing import Optional

from fastapi import HTTPException, Security
from fastapi.security.api_key import APIKeyHeader
from starlette.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED

from fastapi_skeleton.core import config
from fastapi_skeleton.core.messages import AUTH_REQ, NO_API_KEY

api_key = APIKeyHeader(name="token", auto_error=False)


def validate_request(header: Optional[str] = Security(api_key)) -> bool:
if header is None:
raise HTTPException(
status_code=HTTP_400_BAD_REQUEST, detail=NO_API_KEY, headers={}
)
if not secrets.compare_digest(header, str(config.API_KEY)):
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED, detail=AUTH_REQ, headers={}
)
return True
22 changes: 22 additions & 0 deletions fastapi_skeleton/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@


from fastapi import FastAPI

from fastapi_skeleton.api.routes.router import api_router
from fastapi_skeleton.core.config import (API_PREFIX, APP_NAME, APP_VERSION,
IS_DEBUG)
from fastapi_skeleton.core.event_handlers import (start_app_handler,
stop_app_handler)


def get_app() -> FastAPI:
fast_app = FastAPI(title=APP_NAME, version=APP_VERSION, debug=IS_DEBUG)
fast_app.include_router(api_router, prefix=API_PREFIX)

fast_app.add_event_handler("startup", start_app_handler(fast_app))
fast_app.add_event_handler("shutdown", stop_app_handler(fast_app))

return fast_app


app = get_app()
Empty file.
7 changes: 7 additions & 0 deletions fastapi_skeleton/models/heartbeat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@


from pydantic import BaseModel


class HearbeatResult(BaseModel):
is_alive: bool
26 changes: 26 additions & 0 deletions fastapi_skeleton/models/payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

from typing import List
from pydantic import BaseModel


class HousePredictionPayload(BaseModel):
median_income_in_block: float
median_house_age_in_block: int
average_rooms: int
average_bedrooms: int
population_per_block: int
average_house_occupancy: int
block_latitude: float
block_longitude: float


def payload_to_list(hpp: HousePredictionPayload) -> List:
return [
hpp.median_income_in_block,
hpp.median_house_age_in_block,
hpp.average_rooms,
hpp.average_bedrooms,
hpp.population_per_block,
hpp.average_house_occupancy,
hpp.block_latitude,
hpp.block_longitude]
Loading

0 comments on commit dc84eab

Please sign in to comment.