forked from trainindata/deploying-machine-learning-models
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8983db3
commit 7a3df30
Showing
18 changed files
with
438 additions
and
2 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
web: uvicorn app.main:app --host 0.0.0.0 --port $PORT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__version__ = "0.0.2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import json | ||
from typing import Any | ||
|
||
import numpy as np | ||
import pandas as pd | ||
from fastapi import APIRouter, HTTPException | ||
from fastapi.encoders import jsonable_encoder | ||
from loguru import logger | ||
from regression_model import __version__ as model_version | ||
from regression_model.predict import make_prediction | ||
|
||
from app import __version__, schemas | ||
from app.config import settings | ||
|
||
api_router = APIRouter() | ||
|
||
|
||
@api_router.get("/health", response_model=schemas.Health, status_code=200) | ||
def health() -> dict: | ||
""" | ||
Root Get | ||
""" | ||
health = schemas.Health( | ||
name=settings.PROJECT_NAME, api_version=__version__, model_version=model_version | ||
) | ||
|
||
return health.dict() | ||
|
||
|
||
@api_router.post("/predict", response_model=schemas.PredictionResults, status_code=200) | ||
async def predict(input_data: schemas.MultipleHouseDataInputs) -> Any: | ||
""" | ||
Make house price predictions with the TID regression model | ||
""" | ||
|
||
input_df = pd.DataFrame(jsonable_encoder(input_data.inputs)) | ||
|
||
# Advanced: You can improve performance of your API by rewriting the | ||
# `make prediction` function to be async and using await here. | ||
logger.info(f"Making prediction on inputs: {input_data.inputs}") | ||
results = make_prediction(input_data=input_df.replace({np.nan: None})) | ||
|
||
if results["errors"] is not None: | ||
logger.warning(f"Prediction validation error: {results.get('errors')}") | ||
raise HTTPException(status_code=400, detail=json.loads(results["errors"])) | ||
|
||
logger.info(f"Prediction results: {results.get('predictions')}") | ||
|
||
return results |
70 changes: 70 additions & 0 deletions
70
section-06-model-serving-api/house-prices-api/app/config.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import logging | ||
import sys | ||
from types import FrameType | ||
from typing import List, cast | ||
|
||
from loguru import logger | ||
from pydantic import AnyHttpUrl, BaseSettings | ||
|
||
|
||
class LoggingSettings(BaseSettings): | ||
LOGGING_LEVEL: int = logging.INFO # logging levels are type int | ||
|
||
|
||
class Settings(BaseSettings): | ||
API_V1_STR: str = "/api/v1" | ||
|
||
# Meta | ||
logging: LoggingSettings = LoggingSettings() | ||
|
||
# BACKEND_CORS_ORIGINS is a comma-separated list of origins | ||
# e.g: http://localhost,http://localhost:4200,http://localhost:3000 | ||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [ | ||
"http://localhost:3000", # type: ignore | ||
"http://localhost:8000", # type: ignore | ||
"https://localhost:3000", # type: ignore | ||
"https://localhost:8000", # type: ignore | ||
] | ||
|
||
PROJECT_NAME: str = "House Price Prediction API" | ||
|
||
class Config: | ||
case_sensitive = True | ||
|
||
|
||
# See: https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging # noqa | ||
class InterceptHandler(logging.Handler): | ||
def emit(self, record: logging.LogRecord) -> None: # pragma: no cover | ||
# Get corresponding Loguru level if it exists | ||
try: | ||
level = logger.level(record.levelname).name | ||
except ValueError: | ||
level = str(record.levelno) | ||
|
||
# Find caller from where originated the logged message | ||
frame, depth = logging.currentframe(), 2 | ||
while frame.f_code.co_filename == logging.__file__: # noqa: WPS609 | ||
frame = cast(FrameType, frame.f_back) | ||
depth += 1 | ||
|
||
logger.opt(depth=depth, exception=record.exc_info).log( | ||
level, | ||
record.getMessage(), | ||
) | ||
|
||
|
||
def setup_app_logging(config: Settings) -> None: | ||
"""Prepare custom logging for our application.""" | ||
|
||
LOGGERS = ("uvicorn.asgi", "uvicorn.access") | ||
logging.getLogger().handlers = [InterceptHandler()] | ||
for logger_name in LOGGERS: | ||
logging_logger = logging.getLogger(logger_name) | ||
logging_logger.handlers = [InterceptHandler(level=config.logging.LOGGING_LEVEL)] | ||
|
||
logger.configure( | ||
handlers=[{"sink": sys.stderr, "level": config.logging.LOGGING_LEVEL}] | ||
) | ||
|
||
|
||
settings = Settings() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
from typing import Any | ||
|
||
from fastapi import APIRouter, FastAPI, Request | ||
from fastapi.middleware.cors import CORSMiddleware | ||
from fastapi.responses import HTMLResponse | ||
from loguru import logger | ||
|
||
from app.api import api_router | ||
from app.config import settings, setup_app_logging | ||
|
||
# setup logging as early as possible | ||
setup_app_logging(config=settings) | ||
|
||
|
||
app = FastAPI( | ||
title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json" | ||
) | ||
|
||
root_router = APIRouter() | ||
|
||
|
||
@root_router.get("/") | ||
def index(request: Request) -> Any: | ||
"""Basic HTML response.""" | ||
body = ( | ||
"<html>" | ||
"<body style='padding: 10px;'>" | ||
"<h1>Welcome to the API</h1>" | ||
"<div>" | ||
"Check the docs: <a href='/docs'>here</a>" | ||
"</div>" | ||
"</body>" | ||
"</html>" | ||
) | ||
|
||
return HTMLResponse(content=body) | ||
|
||
|
||
app.include_router(api_router, prefix=settings.API_V1_STR) | ||
app.include_router(root_router) | ||
|
||
# Set all CORS enabled origins | ||
if settings.BACKEND_CORS_ORIGINS: | ||
app.add_middleware( | ||
CORSMiddleware, | ||
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], | ||
allow_credentials=True, | ||
allow_methods=["*"], | ||
allow_headers=["*"], | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
# Use this for debugging purposes only | ||
logger.warning("Running in development mode. Do not run like this in production.") | ||
import uvicorn | ||
|
||
uvicorn.run(app, host="localhost", port=8001, log_level="debug") |
2 changes: 2 additions & 0 deletions
2
section-06-model-serving-api/house-prices-api/app/schemas/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from .health import Health | ||
from .predict import MultipleHouseDataInputs, PredictionResults |
7 changes: 7 additions & 0 deletions
7
section-06-model-serving-api/house-prices-api/app/schemas/health.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from pydantic import BaseModel | ||
|
||
|
||
class Health(BaseModel): | ||
name: str | ||
api_version: str | ||
model_version: str |
103 changes: 103 additions & 0 deletions
103
section-06-model-serving-api/house-prices-api/app/schemas/predict.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
from typing import Any, List, Optional | ||
|
||
from pydantic import BaseModel | ||
from regression_model.processing.validation import HouseDataInputSchema | ||
|
||
|
||
class PredictionResults(BaseModel): | ||
errors: Optional[Any] | ||
version: str | ||
predictions: Optional[List[float]] | ||
|
||
|
||
class MultipleHouseDataInputs(BaseModel): | ||
inputs: List[HouseDataInputSchema] | ||
|
||
class Config: | ||
schema_extra = { | ||
"example": { | ||
"inputs": [ | ||
{ | ||
"MSSubClass": 20, | ||
"MSZoning": "RH", | ||
"LotFrontage": 80.0, | ||
"LotArea": 11622, | ||
"Street": "Pave", | ||
"Alley": None, | ||
"LotShape": "Reg", | ||
"LandContour": "Lvl", | ||
"Utilities": "AllPub", | ||
"LotConfig": "Inside", | ||
"LandSlope": "Gtl", | ||
"Neighborhood": "NAmes", | ||
"Condition1": "Feedr", | ||
"Condition2": "Norm", | ||
"BldgType": "1Fam", | ||
"HouseStyle": "1Story", | ||
"OverallQual": 5, | ||
"OverallCond": 6, | ||
"YearBuilt": 1961, | ||
"YearRemodAdd": 1961, | ||
"RoofStyle": "Gable", | ||
"RoofMatl": "CompShg", | ||
"Exterior1st": "VinylSd", | ||
"Exterior2nd": "VinylSd", | ||
"MasVnrType": "None", | ||
"MasVnrArea": 0.0, | ||
"ExterQual": "TA", | ||
"ExterCond": "TA", | ||
"Foundation": "CBlock", | ||
"BsmtQual": "TA", | ||
"BsmtCond": "TA", | ||
"BsmtExposure": "No", | ||
"BsmtFinType1": "Rec", | ||
"BsmtFinSF1": 468.0, | ||
"BsmtFinType2": "LwQ", | ||
"BsmtFinSF2": 144.0, | ||
"BsmtUnfSF": 270.0, | ||
"TotalBsmtSF": 882.0, | ||
"Heating": "GasA", | ||
"HeatingQC": "TA", | ||
"CentralAir": "Y", | ||
"Electrical": "SBrkr", | ||
"FirstFlrSF": 896, | ||
"SecondFlrSF": 0, | ||
"LowQualFinSF": 0, | ||
"GrLivArea": 896, | ||
"BsmtFullBath": 0.0, | ||
"BsmtHalfBath": 0.0, | ||
"FullBath": 1, | ||
"HalfBath": 0, | ||
"BedroomAbvGr": 2, | ||
"KitchenAbvGr": 1, | ||
"KitchenQual": "TA", | ||
"TotRmsAbvGrd": 5, | ||
"Functional": "Typ", | ||
"Fireplaces": 0, | ||
"FireplaceQu": None, | ||
"GarageType": "Attchd", | ||
"GarageYrBlt": 1961.0, | ||
"GarageFinish": "Unf", | ||
"GarageCars": 1.0, | ||
"GarageArea": 730.0, | ||
"GarageQual": "TA", | ||
"GarageCond": "TA", | ||
"PavedDrive": "Y", | ||
"WoodDeckSF": 140, | ||
"OpenPorchSF": 0, | ||
"EnclosedPorch": 0, | ||
"ThreeSsnPortch": 0, | ||
"ScreenPorch": 120, | ||
"PoolArea": 0, | ||
"PoolQC": None, | ||
"Fence": "MnPrv", | ||
"MiscFeature": None, | ||
"MiscVal": 0, | ||
"MoSold": 6, | ||
"YrSold": 2010, | ||
"SaleType": "WD", | ||
"SaleCondition": "Normal", | ||
} | ||
] | ||
} | ||
} |
Empty file.
21 changes: 21 additions & 0 deletions
21
section-06-model-serving-api/house-prices-api/app/tests/conftest.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from typing import Generator | ||
|
||
import pandas as pd | ||
import pytest | ||
from fastapi.testclient import TestClient | ||
from regression_model.config.core import config | ||
from regression_model.processing.data_manager import load_dataset | ||
|
||
from app.main import app | ||
|
||
|
||
@pytest.fixture(scope="module") | ||
def test_data() -> pd.DataFrame: | ||
return load_dataset(file_name=config.app_config.test_data_file) | ||
|
||
|
||
@pytest.fixture() | ||
def client() -> Generator: | ||
with TestClient(app) as _client: | ||
yield _client | ||
app.dependency_overrides = {} |
26 changes: 26 additions & 0 deletions
26
section-06-model-serving-api/house-prices-api/app/tests/test_api.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import math | ||
|
||
import numpy as np | ||
import pandas as pd | ||
from fastapi.testclient import TestClient | ||
|
||
|
||
def test_make_prediction(client: TestClient, test_data: pd.DataFrame) -> None: | ||
# Given | ||
payload = { | ||
# ensure pydantic plays well with np.nan | ||
"inputs": test_data.replace({np.nan: None}).to_dict(orient="records") | ||
} | ||
|
||
# When | ||
response = client.post( | ||
"http://localhost:8001/api/v1/predict", | ||
json=payload, | ||
) | ||
|
||
# Then | ||
assert response.status_code == 200 | ||
prediction_data = response.json() | ||
assert prediction_data["predictions"] | ||
assert prediction_data["errors"] is None | ||
assert math.isclose(prediction_data["predictions"][0], 113422, rel_tol=100) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
[mypy] | ||
plugins = pydantic.mypy | ||
ignore_missing_imports = True | ||
disallow_untyped_defs = True |
8 changes: 8 additions & 0 deletions
8
section-06-model-serving-api/house-prices-api/requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
uvicorn>=0.11.3,<0.12.0 | ||
fastapi>=0.64.0,<1.0.0 | ||
python-multipart>=0.0.5,<0.1.0 | ||
pydantic>=1.8.1,<1.9.0 | ||
typing_extensions>=3.7.4,<3.8.0 | ||
loguru>=0.5.3,<0.6.0 | ||
# We will explain this in the course | ||
tid-regression-model==3.0.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
python-3.9.5 |
11 changes: 11 additions & 0 deletions
11
section-06-model-serving-api/house-prices-api/test_requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
-r requirements.txt | ||
|
||
# testing requirements | ||
pytest>=6.2.3,<6.3.0 | ||
requests>=2.23.0,<2.24.0 | ||
|
||
# repo maintenance tooling | ||
black==20.8b1 | ||
flake8>=3.9.0,<3.10.0 | ||
mypy==0.812 | ||
isort==5.8.0 |
Oops, something went wrong.