Skip to content

Commit

Permalink
Release v1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Jan Forster committed Dec 28, 2023
1 parent f4f1e6e commit 3b78f86
Show file tree
Hide file tree
Showing 23 changed files with 1,502 additions and 105 deletions.
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
IS_DEBUG = False
API_KEY = sample_api_key
DEFAULT_MODEL_PATH=./sample_model/lin_reg_california_housing_model.joblib
IS_DEBUG=False
API_KEY=sample_api_key
DEFAULT_MODEL_PATH=./sample_model/lin_reg_california_housing_model.joblib
122 changes: 122 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
[flake8]

################### PROGRAM ################################

# Specify the number of subprocesses that Flake8 will use to run checks in parallel.
jobs = auto


################### OUTPUT #################################

########## Verbosity ##########

# Increase the verbosity of Flake8’s output.
verbose = 0
# Decrease the verbosity of Flake8’s output.
quiet = 0


########## Formatting ##########

# Select the formatter used to display errors to the user.
format = default

# Print the total number of errors.
count = True
# Print the source code generating the error/warning in question.
show-source = True
# Count the number of occurrences of each error/warning code and print a report.
statistics = True


########## Targets ##########

# Redirect all output to the specified file.
output-file = .flake8.log
# Also print output to stdout if output-file has been configured.
tee = True


################### FILE PATTERNS ##########################

# Provide a comma-separated list of glob patterns to exclude from checks.
exclude =
# git folder
.git,
# python cache
__pycache__,
# pytest cache
.pytest_cache,
# mypy cache
.mypy_cache
# Provide a comma-separate list of glob patterns to include for checks.
filename =
*.py


################### LINTING ################################

########## Environment ##########

# Provide a custom list of builtin functions, objects, names, etc.
builtins =


########## Options ##########

# Report all errors, even if it is on the same line as a `# NOQA` comment.
disable-noqa = False

# Set the maximum length that any line (with some exceptions) may be.
max-line-length = 88
# Set the maximum allowed McCabe complexity value for a block of code.
max-complexity = 10
# Toggle whether pycodestyle should enforce matching the indentation of the opening bracket’s line.
# incluences E131 and E133
hang-closing = True


########## Rules ##########

# ERROR CODES
#
# E/W - PEP8 errors/warnings (pycodestyle)
# F - linting errors (pyflakes)
# C - McCabe complexity error (mccabe)
#
# E133 - closing bracket is missing indentation (conflicts with black)
# E203 - whitespace before ‘:’ (conflicts with black)
# W503 - line break before binary operator
# F401 - module imported but unused
# F403 - ‘from module import *’ used; unable to detect undefined names
#

# Specify a list of codes to ignore.
ignore =
E133,
E203,
W503
# Specify the list of error codes you wish Flake8 to report.
select =
E,
W,
F,
C
# Specify a list of mappings of files and the codes that should be ignored for the entirety of the
# file.
per-file-ignores =
__init__.py:F401,F403

# Enable off-by-default extensions.
enable-extensions =


########## Docstring ##########

# Enable PyFlakes syntax checking of doctests in docstrings.
# doctests = True

# Specify which files are checked by PyFlakes for doctest syntax.
include-in-doctest =
# Specify which files are not to be checked by PyFlakes for doctest syntax.
exclude-in-doctest =
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ wheels/
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
.env

# Installer logs
pip-log.txt
Expand All @@ -41,6 +42,7 @@ pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
htmlcov-py36/
htmlcov-py311/
.tox/
.coverage
.coverage.*
Expand Down
51 changes: 39 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ To experiment and get a feeling on how to use this skeleton, a sample regression

## Requirements

Python 3.6+
- Python 3.11+
- Poetry

## Installation
Install the required packages in your local environment (ideally virtualenv, conda, etc.).

```bash
pip install -r requirements
poetry install
```


Expand All @@ -23,36 +25,61 @@ pip install -r requirements

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:
1. Start your app with:

```bash
set -a
source .env
set +a
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)

![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
## Linting

This skeleton code uses isort, mypy, flake, black, bandit for linting, formatting and static analysis.

Run linting with:

If you're not using `tox`, please install with:
```bash
pip install tox
./scripts/linting.sh
```

Run your tests with:
## Run Tests

Run your tests with:

```bash
tox
./scripts/test.sh
```

This runs tests and coverage for Python 3.6 and Flake8, Autopep8, Bandit.
This runs tests and coverage for Python 3.11 and Flake8, Autopep8, Bandit.


## Changelog

v.1.0.0 - Initial release

- Base functionality for using FastAPI to serve ML models.
- Full test coverage

v.1.1.0 - Update to Python 3.11, FastAPI 0.108.0

- Updated to Python 3.11
- Added linting script
- Updated to pydantic 2.x
- Added poetry as package manager

1 change: 0 additions & 1 deletion fastapi_skeleton/api/routes/heartbeat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

from fastapi import APIRouter

from fastapi_skeleton.models.heartbeat import HearbeatResult
Expand Down
5 changes: 2 additions & 3 deletions fastapi_skeleton/api/routes/prediction.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
@router.post("/predict", response_model=HousePredictionResult, name="predict")
def post_predict(
request: Request,
authenticated: bool = Depends(security.validate_request),
block_data: HousePredictionPayload = None
block_data: HousePredictionPayload,
_: bool = Depends(security.validate_request),
) -> HousePredictionResult:

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

Expand Down
5 changes: 1 addition & 4 deletions fastapi_skeleton/api/routes/router.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@


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")
api_router.include_router(prediction.router, tags=["prediction"], prefix="/model")
2 changes: 0 additions & 2 deletions fastapi_skeleton/core/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


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

Expand Down
4 changes: 2 additions & 2 deletions fastapi_skeleton/core/event_handlers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


from typing import Callable

from fastapi import FastAPI
Expand All @@ -23,11 +21,13 @@ 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
2 changes: 0 additions & 2 deletions fastapi_skeleton/core/security.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


import secrets
from typing import Optional

Expand Down
8 changes: 2 additions & 6 deletions fastapi_skeleton/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@


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)
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:
Expand Down
2 changes: 0 additions & 2 deletions fastapi_skeleton/models/heartbeat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


from pydantic import BaseModel


Expand Down
5 changes: 3 additions & 2 deletions fastapi_skeleton/models/payload.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

from typing import List

from pydantic import BaseModel


Expand All @@ -23,4 +23,5 @@ def payload_to_list(hpp: HousePredictionPayload) -> List:
hpp.population_per_block,
hpp.average_house_occupancy,
hpp.block_latitude,
hpp.block_longitude]
hpp.block_longitude,
]
2 changes: 0 additions & 2 deletions fastapi_skeleton/models/prediction.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


from pydantic import BaseModel


Expand Down
27 changes: 9 additions & 18 deletions fastapi_skeleton/services/models.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@


from typing import List

import joblib
import numpy as np
from loguru import logger

from fastapi_skeleton.core.messages import NO_VALID_PAYLOAD
from fastapi_skeleton.models.payload import (HousePredictionPayload,
payload_to_list)
from fastapi_skeleton.models.payload import HousePredictionPayload, payload_to_list
from fastapi_skeleton.models.prediction import HousePredictionResult


class HousePriceModel(object):
class HousePriceModel:
RESULT_UNIT_FACTOR = 100

RESULT_UNIT_FACTOR = 100000

def __init__(self, path):
def __init__(self, path: str) -> None:
self.path = path
self._load_local_model()

def _load_local_model(self):
def _load_local_model(self) -> None:
self.model = joblib.load(self.path)

def _pre_process(self, payload: HousePredictionPayload) -> List:
def _pre_process(self, payload: HousePredictionPayload) -> np.ndarray:
logger.debug("Pre-processing payload.")
result = np.asarray(payload_to_list(payload)).reshape(1, -1)
return result
Expand All @@ -32,18 +26,15 @@ def _post_process(self, prediction: np.ndarray) -> HousePredictionResult:
logger.debug("Post-processing prediction.")
result = prediction.tolist()
human_readable_unit = result[0] * self.RESULT_UNIT_FACTOR
hpp = HousePredictionResult(median_house_value=human_readable_unit)
hpp = HousePredictionResult(median_house_value=int(human_readable_unit))
return hpp

def _predict(self, features: List) -> np.ndarray:
def _predict(self, features: np.ndarray) -> np.ndarray:
logger.debug("Predicting.")
prediction_result = self.model.predict(features)
return prediction_result

def predict(self, payload: HousePredictionPayload):
if payload is None:
raise ValueError(NO_VALID_PAYLOAD.format(payload))

def predict(self, payload: HousePredictionPayload) -> HousePredictionResult:
pre_processed_payload = self._pre_process(payload)
prediction = self._predict(pre_processed_payload)
logger.info(prediction)
Expand Down
Loading

0 comments on commit 3b78f86

Please sign in to comment.