- Keep it simple and small, avoiding extra complexity at all cost. KISS
- Creation of routes on the fly or by defining regular expressions.
- Support API versions out of the box without altering routes.
- Via decorator or in a defined route, accept only certain HTTP methods.
- Follow the single responsibility principle.
- Be compatible with any WSGI server, example: uWSGI, Gunicorn, Twisted, etc.
- Structured Logging using JSON.
- No template rendering.
- Tracing Request-ID "rid" per request.
- Google App Engine compatible. demo
Documentation : docs.zunzun.io
ZunZuncito is a python module that allows to create and maintain REST API's without hassle.
The simplicity for sketching and debugging helps to develop very fast; versioning is inherit by default, which allows to serve and maintain existing applications, while working in new releases without need to create separate instances. All the applications are WSGI PEP 333 compliant, allowing to migrate existing code to more robust frameworks, without need to modify the existing code.
The idea of creating ZunZuncito, was the need of a very small and light tool (batteries included), that could help to create and deploy REST API's quickly, without forcing the developers to learn or follow a complex flow but, in contrast, from the very beginning, guide them to properly structure their API, giving special attention to "versioned URI's", having with this a solid base that allows to work in different versions within a single ZunZun instance without interrupting service of any existing API resources.
The main application contains a ZunZun instance that must be served by a WSGI compliant server. All requests are later handled by custom python modules; ZunZun is the name of the main class for the zunzuncito module.
All the custom python modules, follow the same structure, they basically consist off a class called APIResource which contains a method called dispatch that will require two arguments: a WSGI environment "environ" as first argument and a function "start_response" that will start the response, see PEP 333
ZunZun core turns around three arguments:
root: directory containing all your API modules - see this like the "document_root"
versions: list of supported versions ['v0', 'v1', 'v2']
routes: list of tuples containing regex patterns, handlers and allowed http methods
In the docs you can find a more detailed overview of the ZunZun arguments and the class itself.
When a new request arrive, the ZunZun router parses the REQUEST_URI in order to accomplish this pattern:
/version/api_resource/path
The router first analyse the URI and determines if it is versioned or not by finding a match with the current specified versions, in case none found, fallback to the default which is always the first item on the versions list in case one provided, or 'v0'.
After this process, the REQUEST_URI becomes a list of resources - something like:
['version, 'api_resource', 'path']
Suppose that the incoming request is:
'http://api.zunzun.io/v1/gevent/ip'
ZunZun will convert it to:
['v1', 'gevent', 'ip']
The second step on the router is to find a match within the routes list and the local modules, but before going further lets see the directory structure for the root (document_root), the first and required argument for the ZunZun class.
my_api |--__init__.py |--v0 | |--__init__.py | |--zun_default | | |--__init__.py | | `--zun_default.py | |--zun_gevent | | |--__init__.py | | `--zun_gevent.py | `--zun_my | |--__init__.py | `--zun_my.py |--v1 | |--__init__.py | |--zun_default | | |--__init__.py | | `--zun_default.py | |--zun_gevent | | |--__init__.py | | `--zun_gevent.py | `--zun_my | |--__init__.py | `--zun_my.py `--v2 |--__init__.py |--zun_default | |--__init__.py | `--zun_default.py |--zun_gevent | |--__init__.py | `--zun_gevent.py `--zun_my |--__init__.py `--zun_my.py
As you can see basically is a directory containing sub-directories which at the end are all python custom modules and can be called in a clean way like:
import my_api.v1.zun_default
notice the prefix zun_
This helps the router to dispatch all the request to an existing module, so continue with the flow, for the incoming request: http://api.zunzun.io/v1/gevent/ip we will try to find a module that matches the API resource 'gevent':
'http://api.zunzun.io/v1/gevent/ip' ==> ['v1', 'gevent', 'ip']
version = v1
api_resource = gevent
path = ip
In case a list of routes is passed as an argument to the ZunZun instance, the router will try to match the api_resource with the items of the routes list. If no matches are found it will try to find the module in the root directory.
The routes format is very simple, it can be something like:
"""
format is:
regex pattern, handler (python module), allowed HTTP methods (defaults to ALL)
"""
[
('/.*', default),
('/test', default, 'POST, PUT, PATCH'),
('(?:[0-9]{1,3}\.){3}[0-9]{1,3}', 'ip', 'GET')
]
Lets suppose this routes were passed to the ZunZun instance, therefore the router would try to found a match between the api_resource gevent in our example with the regex patterns in the list, basically something like:
gevent in: ['/.*', '/test', '(?:[0-9]{1,3}\.){3}[0-9]{1,3}']
if no match is found then the router would try to load the module from the root directory using something like:
import my_api.v1.zun_gevent.zun_gevent
In case it doesn't find a module, an HTTP status 501 Not Implemented code is returned to the client. otherwiste the python module is imported by the router and the request is handled entirely by the imported module
You may ask, why the need of the "zun_" prefix and why not just create a simple structure having the same name that the api_resource.
Well, this is more due the way python import modules, and basically is to avoid collisions by having same modules with the same name. You can change the prefix by passing it as an argument to the ZunZun instance or also disabling it by sending an empty prefix.
In the previous example, the REQUEST_URI contains an APIResource with the word gevent the imported module name is in 'zun_gevent/zun_gevent.py' that gives the flexibility to use the gevent library within your module without creating any conflict, your zun_gevent.py could look something like:
import gevent
import gevent.socket
...
That way you could have any work with gevent or any other API resource having an identical name of your current python modules without any conflict.
Contents of file app.py:
import zunzuncito
root = 'my_api'
versions = ['v0', 'v1', 'v2']
routes = [
('/my', 'ip_tools', 'GET'),
('/status', 'http_status', 'GET'),
('/upload/', 'test_post', 'PUT, POST')
]
app = zunzuncito.ZunZun(root, versions, routes)
Contents of file zun_default.py located in "my_api/zun_default/zun_default.py"
"""
zun_default.py API resource
"""
import json
import logging
from zunzuncito import http_status_codes
from zunzuncito.tools import MethodException, HTTPException, allow_methods
class APIResource(object):
def __init__(self, api):
self.api = api
self.status = 200
self.headers = api.headers.copy()
self.log = logging.getLogger()
self.log.setLevel('INFO')
self.log = logging.LoggerAdapter(
logging.getLogger(), {
'rid': api.request_id,
'indent': 4
})
self.log.info(dict((x, y) for x, y in (
('API', api.version),
('URI', api.URI),
('method', api.method)
)))
@allow_methods('get')
def dispatch(self, environ, start_response):
headers = self.api.headers
start_response(
getattr(http_status_codes, 'HTTP_%d' %
self.status), list(headers.items()))
data = {}
data['about'] = ("Hi %s, I am zunzuncito a micro-framework for creating"
" REST API's, you can read more about me in: "
"www.zunzun.io") % environ.get('REMOTE_ADDR', 0)
data['request-id'] = self.api.request_id
data['URI'] = self.api.URI
data['method'] = self.api.method
return json.dumps(data, sort_keys=True, indent=4)
To run it with gunicorn:
gunicorn -b :8080 -w4 app:app
To run it with uWSGI:
uwsgi --http :8080 --wsgi-file app.py --callable app --master
git clone https://github.com/nbari/zunzuncito.git
python setup.py install
Current demo running on Google App Engine.
available API resources:
- /my
- /status
To get your current IP and location:
http://api.zunzun.io/my
To get only the IP:
http://api.zunzun.io/my/ip
For example to get the meaning of status code 201
http://api.zunzun.io/status/201
Tu have a ZunZun instance up and running in Google App Engine this are the configurations:
Contents of the app.yaml file:
application: <your-GAE-application-id>
version: 1
runtime: python27
api_version: 1
threadsafe: no
handlers:
- url: /favicon\.ico
- static_files: favicon.ico
- upload: favicon\.ico
-
- - url: .*
- script: main.py
Contents of the main.py file:
from google.appengine.ext.webapp.util import run_wsgi_app
import zunzuncito
root = 'my_api'
versions = ['v0', 'v1']
routes = [
('/my/?.*', 'ip_tools', 'GET'),
('/status/?.*', 'http_status', 'GET')
]
app = zunzuncito.ZunZun(root, versions, routes)
run_wsgi_app(app)
Directory structure:
|--app.yaml |--main.py |--favicon.ico |--zunzuncito | |--__init__.py | |--http_status_codes.py | |--tools.py | `--zunzun.py` `--my_api |--v0 | |--__init__.py | |--zun_ip_tools | | |--__init__.py | | `--zun_ip_tools.py | |--zun_http_status | | |--__init__.py | | `zun_http_status.py `--v1 |--__init__.py |--zun_ip_tools | |--__init__.py | `--zun_ip_tools.py `--zun_http_status |--__init__.py `--zun_http_status.py
Basically you just copy the zunzuncito module in to your GAE application directory, define your root, versions and routes, create a ZunZun object and focus more on your API resources (custom python modules)