django-express
==============
|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 <http://opensource.org/licenses/MIT>`__ License.
.. |PyPI-v| image:: https://img.shields.io/pypi/v/django-express.svg
:target: https://pypi.python.org/pypi/django-express
.. |PyPI-pyv| image:: https://img.shields.io/pypi/pyversions/django-express.svg
:target: https://pypi.python.org/pypi/django-express
.. |PypI-djangov| image:: https://img.shields.io/badge/Django-1.8%2C%201.9%2C%201.10-44B78B.svg
:target: https://www.djangoproject.com/