Dynamic API Endpoints
The project follows the standard Django REST Framework “router” logic together with viewsets.
There is one twist. Since all models are dynamically constructed, the view classes, serializer classes and URL’s are also dynamically constructed at startup.
Instances vs classes
Views and serializer objects (i.e., their instances) are always instantiated during a request. What we do differently is that the view and serializer classes are also dynamically constructed.
Serializer Construction
In this project, no handwritten serializer definitions can be found. Instead, the serializer class is dynamically constructed. To demonstrate the basic principle:
MyModel: type[DynamicModel] = model_factory(dataset, table_schema)
MySerializerClass: type[DynamicSerializer] = serializer_factory(MyModel)
@api_view()
def view(request):
queryset = MyModel.objects.all()
serializer = MySerializerClass(data=queryset)
return Response(serializer.data)
The serializer_factory()
function
constructs all fields according to the model layout and schema definition.
Tip
To view what a handwritten serializer would look like,
call manage.py dump_serializers [appname]
from the command line.
The --format=nested
options gives a pseudo-python layout to understand nested relations better.
Viewset Construction
The viewsets are also dynamically constructed. This basically works like this:
MyViewSetClass: type[DynamicApiViewSet] = viewset_factory(MyModel)
router = DefaultRouter()
router.register('urlpath/', MyViewSetClass)
urlpatterns = router.urls
By dynamically constructing the viewset classes (instead of using custom properties)
the view logic integrates nicely with tools like Django’s reverse()
,
DRF’s HyperlinkedRelatedField
and the OpenAPI generator.
Those all assume there are hard-coded view classes and URLs in the project.
The viewset_factory()
function calls various other factories,
including the serializer_factory()
function mentioned above.
Full Initialization
To construct the API, the REST Framework DefaultRouter
class is extended
as DynamicRouter
. It is the startpoint for
all dynamic class construction, including the model, viewsets and serializer classes.
Each construction of these classes follow the same pattern:
there is a factory method and base class that implements most logic in plain Python.
Those base classes can be found in the dso_api.dynamic_api
package:
DynamicFilterSet
The factory methods create a new class, which inherits those base classes and fill in the attributes for the “dataset”, “model” and fields.
When all viewset classes are constructed, reading router.urls
returns all available endpoints
as if it was hard-coded. The urls.py
logic of dso_api.dynamic_api.urls
module
exposes those endpoints to Django.