API Framework
Project structure
Here is the structure of a project generated with generate_api_project
command :
.
├─ template_api # your application package
│ ├─ core # global config and utilities
│ │ ├─ __init__.py
│ │ ├─ config.py
│ │ ├─ event_handlers.py # load your model to your app at startup
│ │ └─ logtools.py
│ │
│ ├─ model # model classes
│ │ ├─ __init__.py
│ │ ├─ model_base.py
│ │ └─ model_gabarit.py
│ │
│ ├─ routers # applications routes
│ │ ├─ schemas
│ │ ├─ __init__.py
│ │ ├─ functional.py
│ │ ├─ technical.py
│ │ └─ utils.py
│ │
│ ├─ __init__.py
│ └─ application.py
│
├─ tests
│ └─ ...
.
.
.
├─ .env # environement variables for settings
├─ makefile
├─ Dockerfile
├─ pyproject.toml # your package dependencies and infos
├─ setup.py
├─ launch.sh # start your application
└─ README.md
Quickstart
Gabarit has generated a template_api
python package that contains all your
FastAPI application logic.
It contains three main sub-packages (cf. project structure) :
-
core
package for configuration and loading your model into your application -
model
package for defining how to download your model, to load it and make predictions -
routers
package for defining your API routes and how they work
Have a look at your .env
file to see the default settings :
APP_NAME="template_api"
API_ENTRYPOINT="/template_api/rs/v1"
MODEL_PATH="template_api-models/model.pkl"
Create a virtualenv and install your package
With make :
Without make :
Start your application
To start your FastAPI application activate your virtual environment and then use the
script launch.sh
or the run
command of the makefile :
This will start a FastAPI thanks to uvicorn that listen on port 5000. Visit http://localhost:5000/docs to see the automatic interactive API documentation (provided by FastAPI and Swagger UI)
How it works
Model class
Your application use a Model
object to make predictions. You will find a base Model
class
in template_api.model.model_base
:
class Model:
def __init__(self):
self._model: Any = None
self._model_conf: dict = None
self._model_explainer = None
self._loaded: bool = False
def is_model_loaded(self) -> bool:
"""return the state of the model"""
return self._loaded
def loading(self, **kwargs):
"""load the model"""
self._model, self._model_conf = self._load_model(**kwargs)
self._loaded = True
def predict(self, *args, **kwargs) -> Any:
"""Make a prediction thanks to the model"""
return self._model.predict(*args, **kwargs)
def explain_as_json(self, *args, **kwargs) -> Union[dict, list]:
"""Compute explanations about a prediction and return a JSON serializable object"""
return self._model_explainer.explain_instance_as_json(*args, **kwargs)
def explain_as_html(self, *args, **kwargs) -> str:
"""Compute explanations about a prediction and return an HTML report"""
return self._model_explainer.explain_instance_as_html(*args, **kwargs)
def _load_model(self, **kwargs) -> Tuple[Any, dict]:
"""Load a model from a file
Returns:
Tuple[Any, dict]: A tuple containing the model and a dict of metadata about it.
"""
...
@staticmethod
def download_model(**kwargs) -> bool:
"""You should implement a download method to automatically download your model"""
...
As you can see, a Model
object has four main attributes :
-
_model
containing your gabarit, scikit-learn or whatever model object -
_model_conf
which is a python dict with metadata about your model -
_model_explainer
containing your model explainer -
_loaded
which is set toTrue
after_load_model
has been called
The Model
class also define a download_model
method that will be used to download
your model. By default it does nothing and returns True
.
You will also find a ModelGabarit
class in template_api.model.model_gabarit
that
is suited to a model constructed thanks to a Gabarit template.
It is a great example of how to adapt the base Model
class to your use case.
Load your model at startup
Your model is loaded into your application at startup thanks to
template_api.core.event_handlers
:
from typing import Callable
from fastapi import FastAPI
from ..model.model_base import Model
def _startup_model(app: FastAPI) -> None:
"""Create and Load model"""
model = Model()
model.loading()
app.state.model = model
def start_app_handler(app: FastAPI) -> Callable:
"""Startup handler: invoke init actions"""
def startup() -> None:
logger.info("Startup Handler: Load model.")
_startup_model(app)
return startup
To change the model used by your application, change the model imported here.
Functional and technical routers
Routers are split into two categories by default : technical and functional ones.
- Technical routers are used for technical purpose such as verifying liveness or getting infos about your application
- Functional ones are used to implement your business logic such as model predictions or model explicability
Since gabarit could not know what data your model is expecting, the default /predict
route
from template_api.routers.functional
use a starlette Request object
instead of pydantic.
For a cleaner way to handle requests and reponses you should use pydantic as stated in FastAPI documentation
You can use routes from template_api.routers.technical as examples of how to create requests and responses schemas thanks to pydantic or have a look at the FastAPI documentation.
Dockerfile
A minimal Dockerfile
is provided by the template. You should have a look a it, especially
if you have to download your model in your containers.