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
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
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 = [ '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.
serializers.py file to
# 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
MLAlgorithm serializers, we defined all read-only fields. This is because, we will create and modify our objects only on the server-side.For
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.
MLAlgorithmSerializer is more complex than others. It has one filed
current_status that represents the latest status from
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
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).
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:
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 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
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
Next step: Serve ML models