smartdjango is a high-level packed developing tools for Django.
It provides the following features:
- Smart middleware for auto packing json-format response
- Smart fields for restricting value, such as min_length of a CharField, or max_value/min_value of an IntegerField
- Smart param for value validating and processing, which can be generated by a field quickly.
- Smart analysis tool for analysing parameters of any method's input, especially the Request object. So the parameters can be formatted/detected before methods running.
- Smart error for identifying, recording and response.
- Smart pager for partly response objects the model filtered.
pip install smartdjango
The latest version is 4.2.1
, building on Aug, 2025. It supports the latest django 5.
Current Version is based on Chinese, as you can see in some readable errors.
error.py
from smartdjango import Error, Code
@Error.register
class BaseErrors:
OK = Error("没有错误", code=Code.OK)
FIELD_VALIDATOR = Error("字段校验器错误", code=Hc.InternalServerError)
FIELD_PROCESSOR = Error("字段处理器错误", code=Code.InternalServerError)
FIELD_FORMAT = Error("字段格式错误", code=Code.BadRequest)
RET_FORMAT = Error("函数返回格式错误", code=Code.InternalServerError)
MISS_PARAM = Error("缺少参数{0}({1})", code=Code.BadRequest)
STRANGE = Error("未知错误", code=Code.InternalServerError)
We will later support more languages.
You need to be familiar with Django development, including models, views as well as basic server development.
We assume you have setup Django environment, with database correctly configured.
JSON is a popular format for data transferring. The following code is usually used to pack dict data to HTTPResponse object:
import json
from django.http import HttpResponse
def handler(request):
data = get_data() # type: dict
return HttpResponse(
json.dumps(data, ensure_ascii=False), # for showing Chinese characters
content_type="application/json; encoding=utf-8",
)
or an easier way:
from django.http import JsonResponse
def handler(request):
data = get_data() # type: dict
return JsonResponse(data)
Now it's the easiest way:
def handler(request):
data = get_data() # type: dict
return data
You may have dozens of handlers, but you don't need to write dozens of
JsonResponse
. What's all you need is to append a single line in
MIDDLEWARES
in setting file like this:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
...
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'smartdjango.middleware.APIPacker',
]
The middleware will directly pass HttpResponse
object, handle errors
which error module creates and importantly pack dict
data.
It's a common requirement to limit the length of username to 3 to 16,
and limit its characters to a
to z
. As CharField
only has
max_length
attribute, what we usually do is:
from django.db import models
class User(models.Model):
username = models.CharField(max_length=16)
password = models.CharField(max_length=16)
...
@classmethod
def new(cls, username, password):
if not cls.check_username_is_alpha(username):
raise Exception
if not cls.check_username_max_min_length(username):
raise Exception
if not cls.check_password_max_min_length(password):
raise Exception
user = cls(username=username, password=password)
user.save()
return user
Now we have a more elegant and magical solution:
from smartdjango import Error, Code
from django.db import models
@Error.register
class UserErrors:
USERNAME_ALPHA = Error("Username should only be consist of alphas.", code=Code.Forbidden)
USERNAME_TOO_SHORT = Error("Username should be at least {length} characters.", code=Code.BadRequest)
PASSWORD_TOO_SHORT = Error("Password should be at least {length} characters.", code=Code.BadRequest)
class UserValidator:
MAX_USERNAME_LENGTH = 16
MIN_USERNAME_LENGTH = 6
MAX_PASSWORD_LENGTH = 16
MIN_PASSWORD_LENGTH = 6
@classmethod
def username(cls, username):
if len(username) < cls.MIN_USERNAME_LENGTH:
raise UserErrors.USERNAME_TOO_SHORT(length=cls.MIN_USERNAME_LENGTH)
@classmethod
def password(cls, password):
if len(password) < cls.MIN_PASSWORD_LENGTH:
raise UserErrors.PASSWORD_TOO_SHORT(length=cls.MIN_PASSWORD_LENGTH)
class User(models.Model):
vldt = UserValidator
username = models.CharField(max_length=vldt.MAX_USERNAME_LENGTH, validators=[vldt.username])
password = models.CharField(max_length=vldt.MAX_PASSWORD_LENGTH, validators=[vldt.password])
...
@classmethod
def new(cls, username, password):
user = cls(username=username, password=password)
user.save()
return user
We firstly create an error class, for storing all errors would faced in
user model. Here USERNAME_ALPHA
is the identifier of this error, with
description and http code.
This error is used in User._valid_username
method. SmartDjango will
detect if there is a attribute-level validator named _valid_ATTRIBUTE
in this class when calling cls.validator
.
If the validator, called in new
method, returns/raises an error, new
method will bubble this error to upper method. As it arrives in
middleware
, it will be handled and shown as json response.
Another thing is max length and min length. cls.validator
will also
detects values if fitting the configuration of fields.
In fact, if the username posted is not valid, we could stop it at the beginning.
Here's the code we usually write in views.py
to get register
information, create a user and return something.
from django.views import View
from User.models import User
import json
class UserView(View):
def post(self, request):
data = json.loads(request)
username = data['username']
password = data['password']
user = User.new(username, password) # assuming we adopt the code above
return dict(id=user.pk, username=user.username) # middleware will handle it!
Try this easier way:
models.py
from diq import Dictify
from smartdjango import Error, Params, Validator
from django.db import models
@Error.register
class UserErrors: ...
class User(models.Model, Dictify):
def _dictify_id(self):
return self.pk
def d(self):
return self.dictify('username', 'id')
...
class UserParams(metaclass=Params):
model_class = User
username: Validator
password: Validator
views.py
from django.views import View
from smartdjango import analyse
from User.models import User
from User.params import UserParams
class UserView(View):
@analyse.json(UserParams.username, UserParams.password)
def post(self, request):
username = request.json.username
password = request.json.password
user = User.new(username, password) # or User.new(**request.json())
return user.d()
As you can see, we support many new features.
Firstly, UserParams
means Parameters created by User. UserParams.username
and UserParams.passowrd
succeed features like max/min length and custom
validator of username and password field.
In views.py
, we use analyse.json
to analyse parameters in request body
,
analyse.query
for query
, and analyse.argument
for argument
. For example, the API:
POST /api/zoo/insect/search?count=5&last=0
{
"name": "fly",
"color": null
}
with the url pattern: path('/api/zoo/<str: kind>/search', some_handler)
, the some handler should be written as:
from smartdjango import analyse, Validator
@analyse.json(
Validator('name', 'name of insert'),
Validator('color').null().default(None)
)
@analyse.query(
Validator('count').to(int).to(lambda x: min(1, x)),
Validator('last').to(int)
)
@analyse.argument('kind')
def some_handler(request, kind):
pass
OK now we back to the UserView
. The analyse
decorator will check
these two parameters if valid. Only if they passed the analysis, they
will be stored in request.data
or request.query/json/argument
object. It's obviously more convenient to use
dot-username
than quote-username
-quote.
Also the dict
method is provided to fetch all/specific parameters.
The next magic is models.dictify
.
It would be very boring to write something like this:
def d(self):
return dict(
id=self.pk,
birthday=self.birthday.strftime('%Y-%m-%d'), # datetime -> str
username=self.username,
description=self.description,
male=self.male,
school=self.school,
...
)
Now dictify
is born, and the bore is gone.
dictify
will detect firstly _dictify_ATTRIBUTE
for each attributes.
If detected, it will record dict(attribute=self._dictify_ATTRIBUTE()),
or else it will find if the model has this attribute.
What we make easy is 2 school
s at beginning but now only once. It can
be simplified like this:
def _dictify_id(self):
return self.pk
def _dictify_birthday(self):
return self.birthday.timestamp()
def d(self):
return self.dictor('id', 'birthday', 'username', 'description', 'male', 'school', ...)
Assuming now you want to get some user's information with its user_id.
Former way:
from smartdjango import analyse
@analyse.query('user_id')
def handler(request):
user_id = request.query.user_id
user = User.get(user_id)
return user.d()
Magic way:
from smartdjango import analyse, Validator
@analyse.query(Validator('user_id', final_name='user').to(User.get))
def handler(r):
return r.d.user.d()
Validator is a simpler limitation compared with Processor. It's a Processor, but the simple version. It will be stored with other processors.
Validator only returns error or None. If None, the value doesn't change. Processor will change value and is able to change parameter's name (final_name).
If some parameters have more than one validators or processors, the
order of those makes a significant effect. Take a timestamp string as an
example, if we want to extract datetime information of the string
"1572451778"
, we firstly need to transfer it as the integer
1572451778
, then use datetime methods to process this integer to a
datetime object. The right way to
handle the problem above should be one of the following codes.
import datetime
from smartdjango import Validator
Validator('time').to(int).to(datetime.datetime.fromtimestamp)
You might think the second method is stupid in some kind, but it's an usual phenomenon. Think about it:
models.py
import datetime
from django.db import models
from smartdjango import Params, Validator
class SomeModel(models.Model):
time = models.DateTimeField(...)
class SomeModelParams(metaclass=Params):
time: Validator
SomeModelParams.time.to(int).to(datetime.datetime.fromtimestamp)
SomeModelParams.time
is created as a Validator
, and its build-in validator will
check if the value is instance of Datetime
. As mentioned above,
validator is stored with other processors (in one processor list), we
need to process the string-format timestamp to datetime type. So what we
always do is add two processors.
Sometimes parameter may have default value or can be null. If some
parameter null
and it posted as null, it will only change it's
final_name
and the value will stay null
. You can use
null(null=False)
to change its state.
If some parameter would have default value, default
will show its
function. When a Validator
doesn't set null but set default, it will get a
default value. For example, Validator('count').default(10)
.
If we want to custom some
Validator
based on it, we can use copy
method to get a new Validator
with all
features.
Rename would be a huge demand, to change parameter's name, read name and yield name.
Django provides convenient filter
method to select items we want based
on fields we limited. But when we do some search things, like get
students whose name contains Jyonn
, we would code like this:
SomeClass.objects.filter(name__contains='Jyonn')
Search takes the secondary place cause name='Jyonn'
means the name is
exactly Jyonn
. Now we want to take search
as important as filter
.
And the dream has come true:
SomeClass.objects.search(name='Jyonn')
We use name__full='Jyonn'
in search
to replace name='Jyonn'
in
filter
. We also support custom design. For example, we want to get all
apps with user number above 500(some digit) and created time in 1
year(some day). Try this:
models.py
import datetime
from SmartDjango import models
class App(models.Model):
user_num = models.IntegerField(...)
create_time = models.DateTimeField(...)
def _search_user_num(self, v):
return dict(user_num__gte=v)
def _search_create_time(self, v):
return dict(create_time__gte=v)
class AppP:
user_num, create_time = App.get_params('user_num', 'create_time')
create_time.processor(datetime.datetime.fromtimestamp, begin=True).processor(int, begin=True)
views.py
from django.views import View
from SmartDjango import Analyse
class SearchView(View):
@staticmethod
@Analyse.r(q=[AppP.user_num, AppP.create_time])
def get(r):
user_num = r.d.user_num
create_time = r.d.create_time
objects = App.search(user_num=user_num, create_time=create_time)
# or
objects = App.search(**r.d.dict())
...
We pack _gte
or some other things into the model, so the code in views
could be quite simple and elegant.
You will find more colorful usage. Try SmartDjango!