Easy Restful APIs with the Django web framework.
Project description
Easy Restful APIs with the Django web framework.
Install
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', ]
Setup
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.
Start serving apis
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.
Adding RESTful services
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
Function as service api
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.
Important Note
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.
Argument APIs
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 (ExpressRequest)
req.params[‘key’]
req.json
req.form
req.files[‘name’]
req.cookies[‘name’]
req[‘HTTP-HEADER’]/req.header(‘key’)
res (ExpressResponse)
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.
Model generated service apis
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.
Decorators
For a function
@service
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> if you mount the ``services.url(s)`` with namespaced ``include()`` calls in ``urls.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.
@methods(m1, m2, …)
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.
@url(path)
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.
@csrf
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.
For a Model
@serve
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 count X-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.
@serve_unprotected
Same as @serve but without csrf protection.
@methods(m1, m2, …)
Same as @methods for a service function.
@url(path)
Same as @url for a service function.
Database Backends
backends.mongodb
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.
Limitation
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.
Licence
Copyright 2017 Tim Lauv. Under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for django_express-0.4.0-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | efb9804bb60a8cb8c0902b14e365f03cd22defbeff7394687c668fdaf5dfc6bb |
|
MD5 | 8247aa8044a00b595131f414b41b8337 |
|
BLAKE2b-256 | ffa1b4f629cb175782ed7f0dd16f46a483f44f4f567748839db01baf12e89e43 |