Skip to main content

Easy Restful APIs with the Django web framework.

Project description

PyPI-v PyPI-pyv PypI-djangov

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Filename, size & hash SHA256 hash help File type Python version Upload date
django_express-0.4.0-py3-none-any.whl (29.5 kB) Copy SHA256 hash SHA256 Wheel py3 Mar 27, 2018
django-express-0.4.0.tar.gz (19.5 kB) Copy SHA256 hash SHA256 Source None Mar 27, 2018

Supported by

Elastic Elastic Search Pingdom Pingdom Monitoring Google Google BigQuery Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN DigiCert DigiCert EV certificate StatusPage StatusPage Status page