What have you already accomplished:
- you have default Django project initialized,
- you have two ML algorithms trained and ready for inference.
What you will learn in this chapter:
- build Django models to store information about ML algorithms and requests in the database,
- write REST API for your ML algorithms with the
Django REST Framework
.
Create Django models
To create Django models we need to create a new app:
# run this in backend/server directory
python manage.py startapp endpoints
mkdir apps
mv endpoints/ apps/
With the above commands, we have created the endpoints
app and moved it to the apps
directory. I have added the apps
directory to keep the project clean.
# list files in apps/endpoints
ls apps/endpoints/
# admin.py apps.py __init__.py migrations models.py tests.py views.py
Let’s go to apps/endpoints/models.py
file and define database models (Django provides object-relational mapping layer (ORM)).
from django.db import models
class Endpoint(models.Model):
'''
The Endpoint object represents ML API endpoint.
Attributes:
name: The name of the endpoint, it will be used in API URL,
owner: The string with owner name,
created_at: The date when endpoint was created.
'''
name = models.CharField(max_length=128)
owner = models.CharField(max_length=128)
created_at = models.DateTimeField(auto_now_add=True, blank=True)
class MLAlgorithm(models.Model):
'''
The MLAlgorithm represent the ML algorithm object.
Attributes:
name: The name of the algorithm.
description: The short description of how the algorithm works.
code: The code of the algorithm.
version: The version of the algorithm similar to software versioning.
owner: The name of the owner.
created_at: The date when MLAlgorithm was added.
parent_endpoint: The reference to the Endpoint.
'''
name = models.CharField(max_length=128)
description = models.CharField(max_length=1000)
code = models.CharField(max_length=50000)
version = models.CharField(max_length=128)
owner = models.CharField(max_length=128)
created_at = models.DateTimeField(auto_now_add=True, blank=True)
parent_endpoint = models.ForeignKey(Endpoint, on_delete=models.CASCADE)
class MLAlgorithmStatus(models.Model):
'''
The MLAlgorithmStatus represent status of the MLAlgorithm which can change during the time.
Attributes:
status: The status of algorithm in the endpoint. Can be: testing, staging, production, ab_testing.
active: The boolean flag which point to currently active status.
created_by: The name of creator.
created_at: The date of status creation.
parent_mlalgorithm: The reference to corresponding MLAlgorithm.
'''
status = models.CharField(max_length=128)
active = models.BooleanField()
created_by = models.CharField(max_length=128)
created_at = models.DateTimeField(auto_now_add=True, blank=True)
parent_mlalgorithm = models.ForeignKey(MLAlgorithm, on_delete=models.CASCADE, related_name = "status")
class MLRequest(models.Model):
'''
The MLRequest will keep information about all requests to ML algorithms.
Attributes:
input_data: The input data to ML algorithm in JSON format.
full_response: The response of the ML algorithm.
response: The response of the ML algorithm in JSON format.
feedback: The feedback about the response in JSON format.
created_at: The date when request was created.
parent_mlalgorithm: The reference to MLAlgorithm used to compute response.
'''
input_data = models.CharField(max_length=10000)
full_response = models.CharField(max_length=10000)
response = models.CharField(max_length=10000)
feedback = models.CharField(max_length=10000, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, blank=True)
parent_mlalgorithm = models.ForeignKey(MLAlgorithm, on_delete=models.CASCADE)
We defined three models:
Endpoint
- to keep information about our endpoints,MLAlgorithm
- to keep information about ML algorithms used in the service,MLAlgorithmStatus
- to keep information about ML algorithm statuses. The status can change in time, for example, we can set testing as initial status and then after testing period switch to production state.MLRequest
- to keep information about all requests to ML algorithms. It will be needed to monitor ML algorithms and run A/B tests.
We need to add our app to INSTALLED_APPS
in backend/server/server/settings.py
, it should look like:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# apps
'apps.endpoints'
]
To apply our models to the database we need to run migrations:
# please run it in backend/server directory
python manage.py makemigrations
python manage.py migrate
The above commands will create tables in the database. By default, Django is using SQLite as a database. For this tutorial, we can keep this simple database, for more advanced projects you can set a Postgres or MySQL as a database (you can configure this by setting DATABASES
variable in backend/server/server/settings.py
).
Create REST API for models
So far we have defined database models, but we will not see anything new when running the web server. We need to specify REST API to our objects. The simplest and cleanest way to achieve this is to use Django REST Framework
(DRF). To install DRF
we need to run:
pip3 install djangorestframework
pip3 install markdown # Markdown support for the browsable API.
pip3 install django-filter # Filtering support
and add it to INSTALLED_APPS
in backend/server/server/settings.py
:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # add django rest framework
# apps
'apps.endpoints'
]
To see something in the browser we need to define:
- serializers - they will define how database objects are mapped in requests,
- views - how our models are accessed in REST API,
- urls - definition of REST API URL addresses for our models.
DRF Serializers
Please add serializers.py
file to server/apps/endpoints
directory:
# backend/server/apps/endpoints/serializers.py file
from rest_framework import serializers
from apps.endpoints.models import Endpoint
from apps.endpoints.models import MLAlgorithm
from apps.endpoints.models import MLAlgorithmStatus
from apps.endpoints.models import MLRequest
class EndpointSerializer(serializers.ModelSerializer):
class Meta:
model = Endpoint
read_only_fields = ("id", "name", "owner", "created_at")
fields = read_only_fields
class MLAlgorithmSerializer(serializers.ModelSerializer):
current_status = serializers.SerializerMethodField(read_only=True)
def get_current_status(self, mlalgorithm):
return MLAlgorithmStatus.objects.filter(parent_mlalgorithm=mlalgorithm).latest('created_at').status
class Meta:
model = MLAlgorithm
read_only_fields = ("id", "name", "description", "code",
"version", "owner", "created_at",
"parent_endpoint", "current_status")
fields = read_only_fields
class MLAlgorithmStatusSerializer(serializers.ModelSerializer):
class Meta:
model = MLAlgorithmStatus
read_only_fields = ("id", "active")
fields = ("id", "active", "status", "created_by", "created_at",
"parent_mlalgorithm")
class MLRequestSerializer(serializers.ModelSerializer):
class Meta:
model = MLRequest
read_only_fields = (
"id",
"input_data",
"full_response",
"response",
"created_at",
"parent_mlalgorithm",
)
fields = (
"id",
"input_data",
"full_response",
"response",
"feedback",
"created_at",
"parent_mlalgorithm",
)
Serializers will help with packing and unpacking database objects into JSON objects. In Endpoints
and MLAlgorithm
serializers, we defined all read-only fields. This is because, we will create and modify our objects only on the server-side.For MLAlgorithmStatus
, fields status
, created_by
, created_at
and parent_mlalgorithm
are in read and write mode, we will use the to set algorithm status by REST API. For MLRequest
serializer there is a feedback
field that is left in read and write mode - it will be needed to provide feedback about predictions to the server.
The MLAlgorithmSerializer
is more complex than others. It has one filed current_status
that represents the latest status from MLAlgorithmStatus
.
Views
To add views please open backend/server/endpoints/views.py
file and add the following code:
# backend/server/apps/endpoints/views.py file
from rest_framework import viewsets
from rest_framework import mixins
from apps.endpoints.models import Endpoint
from apps.endpoints.serializers import EndpointSerializer
from apps.endpoints.models import MLAlgorithm
from apps.endpoints.serializers import MLAlgorithmSerializer
from apps.endpoints.models import MLAlgorithmStatus
from apps.endpoints.serializers import MLAlgorithmStatusSerializer
from apps.endpoints.models import MLRequest
from apps.endpoints.serializers import MLRequestSerializer
class EndpointViewSet(
mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet
):
serializer_class = EndpointSerializer
queryset = Endpoint.objects.all()
class MLAlgorithmViewSet(
mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet
):
serializer_class = MLAlgorithmSerializer
queryset = MLAlgorithm.objects.all()
def deactivate_other_statuses(instance):
old_statuses = MLAlgorithmStatus.objects.filter(parent_mlalgorithm = instance.parent_mlalgorithm,
created_at__lt=instance.created_at,
active=True)
for i in range(len(old_statuses)):
old_statuses[i].active = False
MLAlgorithmStatus.objects.bulk_update(old_statuses, ["active"])
class MLAlgorithmStatusViewSet(
mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet,
mixins.CreateModelMixin
):
serializer_class = MLAlgorithmStatusSerializer
queryset = MLAlgorithmStatus.objects.all()
def perform_create(self, serializer):
try:
with transaction.atomic():
instance = serializer.save(active=True)
# set active=False for other statuses
deactivate_other_statuses(instance)
except Exception as e:
raise APIException(str(e))
class MLRequestViewSet(
mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet,
mixins.UpdateModelMixin
):
serializer_class = MLRequestSerializer
queryset = MLRequest.objects.all()
For each model, we created a view which will allow to retrieve single object or list of objects. We will not allow to create or modify Endpoints
, MLAlgorithms
by REST API. The code to to handle creation of new ML related objects will be on server side, I will describe it in the next chapter.
We will allow to create MLAlgorithmStatus
objects by REST API. We don’t allow to edit statuses for ML algorithms as we want to keep all status history.
We allow to edit MLRequest
objects, however only feedback
field (please take a look at serializer definition).
URLs
The last step is to add URLs to access out models. Please add urls.py
file in backend/server/apps/endpoints
with following code:
# backend/server/apps/endpoints/urls.py file
from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter
from apps.endpoints.views import EndpointViewSet
from apps.endpoints.views import MLAlgorithmViewSet
from apps.endpoints.views import MLAlgorithmStatusViewSet
from apps.endpoints.views import MLRequestViewSet
router = DefaultRouter(trailing_slash=False)
router.register(r"endpoints", EndpointViewSet, basename="endpoints")
router.register(r"mlalgorithms", MLAlgorithmViewSet, basename="mlalgorithms")
router.register(r"mlalgorithmstatuses", MLAlgorithmStatusViewSet, basename="mlalgorithmstatuses")
router.register(r"mlrequests", MLRequestViewSet, basename="mlrequests")
urlpatterns = [
url(r"^api/v1/", include(router.urls)),
]
The above code will create REST API routers to our database models. Our models will be accessed by following the URL pattern:
http://<server-ip>/api/v1/<object-name>
You can notice that we include v1
in the API address. This might be needed later for API versioning.
We need to add endpoints urls to main urls.py
file of the server (file backend/server/server/urls.py
):
# backend/server/server/urls.py file
from django.conf.urls import url, include
from django.contrib import admin
from django.urls import path
from apps.endpoints.urls import urlpatterns as endpoints_urlpatterns
urlpatterns = [
path('admin/', admin.site.urls),
]
urlpatterns += endpoints_urlpatterns
Run the server
We have added many new things, let’s check if all works.
Please run the server:
# in backend/server
python manage.py runserver
and open http://127.0.0.1:8000/api/v1/
in the web browser. You should see DRF view.
The DRF provides nice interface, so you can click on any URL and check the objects (for example on http://127.0.0.1:8000/api/v1/endpoints). You should see empty list for all objects, because we didn’t add anything there yet. We will add ML algorithms and endpoints in the next chapter.
Add code to the repository
The last step in this chapter is to add a new code to the repository.
# please run in backend/server directory
git add apps/endpoints
git commit -am "endpoints models"
git push