Reusable code¶
One of the worst things about the current generation of Python REST frameworks is code re-usability.
django-rest-frameworkis very flexible, but all the flexibility comes from importing fully qualified object’s path strings taken from app’s settings. It is very hard to properly type a code base like this. Using it is also really hard, because you can’t easily navigate in your source code.fastapidoes not even offer a way to write reusable code, because it is based on functions, which are really hard to reuse and modify. That’s why you have to copy paste lots of code just to, for example, use the most common things such as JWT auth.
What does django-modern-rest offer instead?
Reusable controllers¶
We offer a concept of a “reusable controllers”.
To make a reusable controller, you need
to provide typing.TypeVar instead of a
real BaseSerializer type.
Here’s an example:
1from typing import TypeVar
2
3from typing_extensions import TypedDict
4
5from dmr import Controller
6from dmr.serializer import BaseSerializer
7
8_SerializerT = TypeVar('_SerializerT', bound=BaseSerializer)
9
10
11class _ResponseBody(TypedDict):
12 message: str
13
14
15class ReusableController(Controller[_SerializerT]):
16 def get(self) -> _ResponseBody:
17 serializer_name = self.serializer.__name__
18 return {'message': f'hello from {serializer_name}'}
This code can work with both pydantic and msgspec as serializers.
Let’s try to create two exact controllers with exact serializers:
1from dmr.plugins.msgspec import MsgspecSerializer
2from examples.reusable_code.reusable_controller import ReusableController
3
4
5class MsgspecController(ReusableController[MsgspecSerializer]):
6 """This controller will use msgspec for serialization."""
7
Run result
$ curl http://127.0.0.1:8000/api/example/ -X GET
{"message":"hello from MsgspecSerializer"}
1from dmr.plugins.pydantic import PydanticSerializer
2from examples.reusable_code.reusable_controller import ReusableController
3
4
5class PydanticController(ReusableController[PydanticSerializer]):
6 """This controller will use pydantic for serialization."""
7
Run result
$ curl http://127.0.0.1:8000/api/example/ -X GET
{"message":"hello from PydanticSerializer"}
Basically - we just specify what kind of serializer to use. And that’s it. But, this is just the first step. We can do much more!
Generic parsing and response models¶
Next, let’s define a reusable controller that will have:
customizable serializer
customizable request model
customizable response body
The process will look exactly the same:
1from abc import abstractmethod
2from typing import Generic, TypeVar
3
4from dmr import Body, Controller
5from dmr.serializer import BaseSerializer
6
7_SerializerT = TypeVar('_SerializerT', bound=BaseSerializer)
8_RequestModelT = TypeVar('_RequestModelT')
9_ResponseBodyT = TypeVar('_ResponseBodyT')
10
11
12class ReusableController(
13 Controller[_SerializerT],
14 Generic[_SerializerT, _RequestModelT, _ResponseBodyT],
15):
16 def post(self, parsed_body: Body[_RequestModelT]) -> _ResponseBodyT:
17 return self.convert(parsed_body)
18
19 @abstractmethod
20 def convert(self, parsed_body: _RequestModelT) -> _ResponseBodyT:
21 raise NotImplementedError
Here we use 3 type variables. One of each of the parts we want to customize.
Important part here is that we defined our own abstract convert method
to convert unknown request model into an unknown response body.
We would need to implement this method in all of our concrete controllers.
1from typing_extensions import TypedDict, override
2
3from dmr.plugins.msgspec import MsgspecSerializer
4from examples.reusable_code.reusable_parsing import ReusableController
5
6
7class _RequestModel(TypedDict):
8 username: str
9
10
11class _ResponseBody(TypedDict):
12 message: str
13
14
15class MsgspecController(
16 ReusableController[MsgspecSerializer, _RequestModel, _ResponseBody],
17):
18 @override
19 def convert(self, parsed_body: _RequestModel) -> _ResponseBody:
20 return {'message': f'Hello, {parsed_body["username"]}'}
21
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"username": "sobolevn"}' -H 'Content-Type: application/json'
{"message":"Hello, sobolevn"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_RequestModel": {
"properties": {
"username": {
"type": "string"
}
},
"required": [
"username"
],
"title": "_RequestModel",
"type": "object"
},
"_ResponseBody": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"title": "_ResponseBody",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/msgspeccontroller/": {
"post": {
"deprecated": false,
"operationId": "postMsgspeccontrollerApiMsgspeccontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseBody"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1from typing_extensions import TypedDict, override
2
3from dmr.plugins.pydantic import PydanticSerializer
4from examples.reusable_code.reusable_parsing import ReusableController
5
6
7class _RequestModel(TypedDict):
8 first_name: str
9 last_name: str
10
11
12class _ResponseBody(TypedDict):
13 full_name: str
14
15
16class PydanticController(
17 ReusableController[PydanticSerializer, _RequestModel, _ResponseBody],
18):
19 @override
20 def convert(self, parsed_body: _RequestModel) -> _ResponseBody:
21 return {
22 'full_name': (
23 f'{parsed_body["first_name"]} {parsed_body["last_name"]}'
24 ),
25 }
26
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"first_name": "Nikita", "last_name": "Sobolev"}' -H 'Content-Type: application/json'
{"full_name":"Nikita Sobolev"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_RequestModel": {
"properties": {
"first_name": {
"title": "First Name",
"type": "string"
},
"last_name": {
"title": "Last Name",
"type": "string"
}
},
"required": [
"first_name",
"last_name"
],
"title": "_RequestModel",
"type": "object"
},
"_ResponseBody": {
"properties": {
"full_name": {
"title": "Full Name",
"type": "string"
}
},
"required": [
"full_name"
],
"title": "_ResponseBody",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/pydanticcontroller/": {
"post": {
"deprecated": false,
"operationId": "postPydanticcontrollerApiPydanticcontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseBody"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Note that msgspec and pydantic controllers in this
case have completely different request and response bodies
and completely different OpenAPI schemas.
We can completely customize each controller and all parsing components and return type validation.
Important
All schema generation and validation rules work the same way for concrete controllers.
We infer the passed values during import time and use real types.