Easy Restful APIs with the Django web framework.
Download through pip
(virtualenv -p python3 .venv)
pip install django-express
Add it to your INSTALLED_APPS
in settings.py
INSTALLED_APPS = [
# ...
'django.contrib.staticfiles',
'express',
]
Mount the auto-discovered services to any entry point (url) you want in urlpatterns
# proj/proj/urls.py
from django.conf.urls import url, include
from express import services
urlpatterns = [
url(r'^api/v1/', include(services.urls)) # mount everything
url(r'^app-name/api/v1/', include(services.url('app-name', ...))) # mount only those from specific app(s)
]
Please double check if your url()
call here has the path argument ending with a trailing slash (e.g foo/bar/
). This is required by the Django framework. You do not need to have this in your @url()
decorator paths though.
You can just start Django like normal, your apis will be automatically discovered and mounted.
./manage.py runserver 0.0.0.0:8000
Note that for other developers to use your apis, you need to bind on wildcard or public WAN/LAN accessable IP address specifically after runserver
instead of leaving the param out to use the default 127.0.0.1
localhost IP. If you are developing inside a VM (e.g through our Vagrant web dev vm) it is very important that you specify the VM's IP or the wildcard IP after runserver
so that you can use your host machine's browser for accessing the apis through vm to host forwarded ports.
Also, use runserver
with DEBUG=true
in settings.py
will automatically serve all the static/
sub-folders (and those added by STATICFILES_DIRS
) from your apps. They are served like they were merged under the same uri set by STATIC_URL
in your settings.py
, so if you do not want files from different apps to override each other, put all your static assets (e.g *.js/html/css/png) in a sub-folder with the same name as your app inside each static/
. Though STATIC_URL
doesn't have a default value, after running django-admin startproject
your settings.py
will set a default value /static/
to it so you could access files from the static/
sub-folders under http://domain:8000/static/<file path>
with zero setup time.
If you are not using the runserver
command for serving static assets and service apis during development, make sure you call ./manage.py collectstatics
and serve folder STATIC_ROOT
on STATIC_URL
, so that {% load static %}
then {% static "images/hi.jpg" %}
can work properly in your templates.
Create apps in your Django project normally, this is to sub-divide your services by app name for better maintainability. Optional though.
./manage.py startapp app_example
./manage.py startapp another_app_with_services
Add a services.py
file in each app folder containing the service functions fn(req, res, *args, **kwargs)
decorated with @service
# proj/app_example/services.py
from express.decorators import service, methods, url
# /api/v1/absolute/url
# /api/v1/app_example/relative/abcd
@methods('GET', 'POST')
@url('/absolute/url')
@url('relative/abcd')
@service
def abc(req, res, *args, **kwargs):
res.json({'json': req.json, 'link:': reverse('express:testa.abc')})
# /api/v1/app_example/efg
@service
def efg(req, res, *args, **kwargs):
res.json({
'params': dict(req.params.lists()), # use {**req.params} in python 3.5+
'form': dict(req.form.lists()), # use {**req.form} in python 3.5+
'json': req.json,
'mime': req['CONTENT_TYPE'],
})
# /api/v1/app_example/hij
@service
def hij(req, res, *args, **kwargs):
res.file('db.sqlite3')
# /api/v1/app_example/x
@service
def x(req, res, *args, **kwargs):
#res.text('Nothing but a test from {}'.format(__name__))
res.text('<p>Agent: {}</p>'.format(req['HTTP_USER_AGENT']))
res.html('<p>IP: {}</p>'.format(req['REMOTE_ADDR']))
res.text('<p>Method: {}</p>'.format(req['REQUEST_METHOD']))
# /api/v1/app_example/relative/url/y-service/articles/2017/01/
@url('relative/url/y-service/articles/([0-9]{4})/([0-9]{2})/')
@service
def y1(req, res, y, m, *args, **kwargs):
res.json({
'data': 'Nothing but a test from {}.{}'.format(__name__, 'y1 - positional capture'),
'text': 123,
'year': y,
'month': m,
})
res.header('Hello~', 'World!') # header
res.status(201) # status
# /api/v1/app_example/z
@service
def z(req, res, *args, **kwargs):
res.download('db.sqlite3')
As you can see, you can still use regex captures in @url('..path..')
if prefered. The captured group/named group will be passed normally to your service function as positional args and keyword args. However, You can NOT use both positioned and namged group captures in the same url!! Due to django implementation.
Put @service
as the inner-most decorator, other decorators don't have this hard requirement on ordering here. You can still use all
the decorators from the Django web framework like @permission_required
or @login_required
but make sure they are all above @service
.
The most important arguments to your service function would be the first two, namely req
for request and res
for response. Here are the available methods on these two objects.
- req.params['key']
- req.json
- req.form
- req.files['name']
- req.cookies['name']
- req['HTTP-HEADER']/req.header('key')
- res.redirect('url')
- res.render(req, 'template', context={})
- res.html('str')/text('str')
- res.json(dict)
- res.file('path')
- res.attach('path')/download('path')
- res.status(int)
- res['HTTP_HEADER']/res.header('key', val)
Caveat: res.status()
and res['HTTP_HEADER']/res.header()
must be called after .render()/html()/text()/json()/file()/attach()/download()
in your service function for new headers and status to be applied to the response.
Within the models.py
file, you can decorate any of your Model class directly for it to generate the apis around its CRUD database operations.
# proj/app_example/models.py
@url('/absolute/db/device')
@url('db/device')
@serve_unprotected
class Device(models.Model):
"""docstring for Device"""
sn = models.CharField(max_length=32)
This will mount 5 default service functions bound to different HTTP methods (POST/GET/PUT,PATCH/DELETE/HEAD) to url app_example/models/Device
for its CRUD database operations and one more metadata operations.
Turn your fn(req, res, *args, **kwargs)
function into a Restful service routine. Automatically detected if present in services.py
in any installed app.
- Default path with
services.urls
:/<app>/<fn>
- Default path with
services.url(app, noprefix=True)
:/<fn>
You can change the mounting path by using the @url()
decorator. You can also use django.urls.reverse()
to get the mount point by name <namespace>:<app>.services.<fn>
.
Still, do not forget to mount everthing collected inside services.urls
to a root url in the django urls.py
. See the Setup section above.
Allowed HTTP request methods to the service. You can also use @safe
to allow only GET
and HEAD
requests. You can use different @methods()
on each service function with the same @url()
path to reuse the same url.
Override basic service auto-path (/<app>/<fn>
). No need to use r'..path..'
here, what you put in path
will be treated as raw string automatically. Feel free to put regex group captures. Just don't mix named and annonymous capture groups in the url path, they won't work together in django.
You can use multiple @url()
on the same service function.
Setting CSRF token cookie on GET/HEAD
requests to the service. Checks and rejects POST/PUT/PATCH/DELETE
requests according to their csrf token + cookie pairs.
If you want an Ajax request to be guarded by django CSRF (django.middleware.csrf.CsrfViewMiddleware) you need to GET/HEAD
the @csrf
decorated service first to have your CSRF cookie (named csrftoken
) set, then POST/PUT/PATCH/DELETE
to it with real requests including either X-CSRFToken
in header or csrfmiddlewaretoken
in a hidden form <input>
field. The header or hidden field value should match the value given by the cookie.
You can change the cookie and header names but NOT the hidden field name in the django settings.py
.
Give a Model default RESTful apis to its CRUD operations.
- Default path with
services.urls
:/<app>/<Model>
- Default path wiht
services.url(app, noprefix=True)
:/<Model>
You can change the mounting path by using the @url()
decorator. You can also use django.urls.reverse()
to get the mount point by name <namespace>:<app>.models.<fn>
.
- POST -- create -- {"payload": {...data...}}
- GET -- read -- ?pk= for single record, omit for all
- PUT/PATCH -- update -- {"payload": {"id": "...", ...data...}}
- DELETE -- delete -- ?pk= for target record, required
- HEAD -- meta -- model name
X-Django-App-Model
and table countX-DB-Table-Count
in reply headers
When using GET http request on a @serve
(-ed) model, you can also specify params for filtering (by columns and Django ORM filter operations), sorting (by columns) and paging the returned result.
?filter=foo1:op_and_val1&filter=foo2:op_and_val2
?sort=foo, -bar
?size=number
?offset=number
?page=number
When using Any http requests on a @serve
(-ed) model, you can always use ?db=...
to switch onto the specific database for served model apis to query and modify. The database names come from your DATABASES
configure in settings.py
.
Still, do not forget to mount everthing collected inside services.urls
to a root url in the django urls.py
. See the Setup section above.
Same as @serve but without csrf protection.
Same as @methods for a service function.
Same as @url for a service function.
This is a dummy backend engine to use with MongoDB connections without the involvement of Django ORM. The purpose is to have your MongoDB settings in the settings.py
and use django.db.connections['<your mongodb name>']
to start using MongoDB in your Django apps.
# settings.py
DATABASES = {
...,
'mongo': {
'ENGINE': 'express.db.backends.mongodb',
'HOST': 'mongo.server.com',
'PORT': 27017,
'NAME': 'testdb',
'USER': '...',
'PASSWORD': '...',
'OPTIONS': {
...pymongo.MongoClient options...
}
},
...
}
Now you will have,
- django.db.connections['testdb'].db - a
pymongo
db object; - django.db.connections['testdb'].collection('collection'=None) - a
pymongo
collection or all available collection names; - django.db.connections['testdb'].cursor('collection', **kwargs) - a .find(kwargs)
pymongo
cursor;
After getting the above, you will have,
- django.db.connections['testdb'].connection - a
pymongo
client;
Use .cursor()
for search (GET
) apis and .collection()
for modify (POST/PUT/PATCH/DELETE
) apis.
This engine works up to the point of creating the db connection and collection cursor, taking in DATABASES options from your settings.py; The ORM layer (migration, schema, transactions, save/delete()) will not work on database that has settings using this Engine.
Copyright 2017 Tim Lauv. Under the MIT License.