├── LICENSE ├── MANIFEST.in ├── README.rst ├── flask_rest.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 by Alexis Métaireau and contributors. See AUTHORS 2 | for more details. 3 | 4 | Some rights reserved. 5 | 6 | Redistribution and use in source and binary forms of the software as well 7 | as documentation, with or without modification, are permitted provided 8 | that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 24 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 33 | DAMAGE. 34 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask REST 2 | ########## 3 | 4 | This library is a tiny REST toolkit intending to simplify your life when you 5 | want to create a REST API for your flask apps. 6 | 7 | Install it 8 | ========== 9 | 10 | Well, that's really simple, it's packaged and on PyPI, so:: 11 | 12 | $ pip install flask-rest 13 | 14 | Use it 15 | ====== 16 | 17 | Handlers 18 | -------- 19 | 20 | Create your classes with specific methods (namely add, get, delete and update), 21 | register it with an url, and you're good. 22 | 23 | 24 | Here is a simple example on how to use it:: 25 | 26 | from flask import Blueprint 27 | from flask_rest import RESTResource, need_auth 28 | 29 | # Subclass a RestResource and configure it 30 | 31 | api = Blueprint("api", __name__, url_prefix="/api") 32 | 33 | # You can define a authenfier if you want to. 34 | 35 | class ProjectHandler(object): 36 | 37 | def add(self): #This maps on "post /" 38 | form = ProjectForm(csrf_enabled=False) # just for the example 39 | if form.validate(): 40 | project = form.save() 41 | db.session.add(project) 42 | db.session.commit() 43 | return 201, project.id 44 | return 400, form.errors # returns a status code and the data 45 | 46 | def get(self, project_id): # maps on GET / 47 | # do your stuff here 48 | return 200, project 49 | 50 | # you can use the "need_auth" decorator to do things for you 51 | @need_auth(authentifier_callable, "project") # injects the "project" argument if authorised 52 | def delete(self, project): 53 | # do your stuff 54 | return 200, "DELETED" 55 | 56 | 57 | Once your handlers defined, you just have to register them with the app or the 58 | blueprint:: 59 | 60 | project_resource = RESTResource( 61 | name="project", # name of the var to inject to the methods 62 | route="/projects", # will be availble at /api/projects/* 63 | app=api, # the app which should handle this 64 | actions=["add", "update", "delete", "get"], #authorised actions 65 | handler=ProjectHandler()) # the handler of the request 66 | 67 | If everything should be protected, you can use the `authentifier` argument:: 68 | 69 | authentifier=check_project 70 | 71 | Where `check_project` is a callable that returns either the project or False if 72 | the acces is not authorized. 73 | 74 | Serialisation / Deserialisation 75 | ------------------------------- 76 | 77 | When you are returning python objects, they can be serialized, which could be 78 | useful in most of the cases. The only serialisation format supported so far is 79 | JSON. 80 | 81 | To serialise *normal* python objects, they should have a `_to_serialize` 82 | attribute, containing all the names of the attributes to serialize. Here is an 83 | example:: 84 | 85 | 86 | class Member(): 87 | 88 | _to_serialize = ("id", "name", "email") 89 | 90 | def __init__(self, **kwargs): 91 | for name, value in kwargs.items(): 92 | setattr(self, name, value) 93 | 94 | If you want to have a look at a real use for this, please head to 95 | https://github.com/spiral-project/ihatemoney/blob/master/budget/api.py 96 | -------------------------------------------------------------------------------- /flask_rest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from flask import request 3 | import werkzeug 4 | 5 | class RESTResource(object): 6 | """Represents a REST resource, with the different HTTP verbs""" 7 | _NEED_ID = ["get", "update", "delete"] 8 | _VERBS = {"get": "GET", 9 | "update": "PUT", 10 | "delete": "DELETE", 11 | "list": "GET", 12 | "add": "POST",} 13 | 14 | def __init__(self, name, route, app, handler, authentifier=None, 15 | actions=None, inject_name=None): 16 | """ 17 | :name: 18 | name of the resource. This is being used when registering 19 | the route, for its name and for the name of the id parameter 20 | that will be passed to the views 21 | 22 | :route: 23 | Default route for this resource 24 | 25 | :app: 26 | Application to register the routes onto 27 | 28 | :actions: 29 | Authorized actions. Optional. None means all. 30 | 31 | :handler: 32 | The handler instance which will handle the requests 33 | 34 | :authentifier: 35 | callable checking the authentication. If specified, all the 36 | methods will be checked against it. 37 | """ 38 | if not actions: 39 | actions = self._VERBS.keys() 40 | 41 | self._route = route 42 | self._handler = handler 43 | self._name = name 44 | self._identifier = "%s_id" % name 45 | self._authentifier = authentifier 46 | self._inject_name = inject_name # FIXME 47 | 48 | for action in actions: 49 | self.add_url_rule(app, action) 50 | 51 | def _get_route_for(self, action): 52 | """Return the complete URL for this action. 53 | 54 | Basically: 55 | 56 | - get, update and delete need an id 57 | - add and list does not 58 | """ 59 | route = self._route 60 | 61 | if action in self._NEED_ID: 62 | route += "/<%s>" % self._identifier 63 | 64 | return route 65 | 66 | def add_url_rule(self, app, action): 67 | """Registers a new url to the given application, regarding 68 | the action. 69 | """ 70 | method = getattr(self._handler, action) 71 | 72 | # decorate the view 73 | if self._authentifier: 74 | method = need_auth(self._authentifier, 75 | self._inject_name or self._name)(method) 76 | 77 | method = serialize(method) 78 | 79 | app.add_url_rule( 80 | self._get_route_for(action), 81 | "%s_%s" % (self._name, action), 82 | method, 83 | methods=[self._VERBS.get(action, "GET")]) 84 | 85 | 86 | def need_auth(authentifier, name=None, remove_attr=True): 87 | """Decorator checking that the authentifier does not returns false in 88 | the current context. 89 | 90 | If the request is authorized, the object returned by the authentifier 91 | is added to the kwargs of the method. 92 | 93 | If not, issue a 401 Unauthorized error 94 | 95 | :authentifier: 96 | The callable to check the context onto. 97 | 98 | :name: 99 | **Optional**, name of the argument to put the object into. 100 | If it is not provided, nothing will be added to the kwargs 101 | of the decorated function 102 | 103 | :remove_attr: 104 | Remove or not the `*name*_id` from the kwargs before calling the 105 | function 106 | """ 107 | def wrapper(func): 108 | def wrapped(*args, **kwargs): 109 | result = authentifier(*args, **kwargs) 110 | if result: 111 | if name: 112 | kwargs[name] = result 113 | if remove_attr: 114 | del kwargs["%s_id" % name] 115 | return func(*args, **kwargs) 116 | else: 117 | return 401, "Unauthorized" 118 | return wrapped 119 | return wrapper 120 | 121 | # serializers 122 | 123 | def serialize(func): 124 | """If the object returned by the view is not already a Response, serialize 125 | it using the ACCEPT header and return it. 126 | """ 127 | def wrapped(*args, **kwargs): 128 | # get the mimetype 129 | mime = request.accept_mimetypes.best_match(SERIALIZERS.keys()) or "application/json" 130 | data = func(*args, **kwargs) 131 | serializer = SERIALIZERS[mime] 132 | 133 | status = 200 134 | if len(data) == 2: 135 | status, data = data 136 | 137 | # serialize it 138 | return werkzeug.Response(serializer.encode(data), 139 | status=status, mimetype=mime) 140 | 141 | return wrapped 142 | 143 | 144 | class JSONEncoder(json.JSONEncoder): 145 | """Subclass of the default encoder to support custom objects""" 146 | def default(self, o): 147 | if hasattr(o, "_to_serialize"): 148 | # build up the object 149 | data = {} 150 | for attr in o._to_serialize: 151 | data[attr] = getattr(o, attr) 152 | return data 153 | elif hasattr(o, "isoformat"): 154 | return o.isoformat() 155 | else: 156 | try: 157 | from flask_babel import speaklater 158 | if isinstance(o, speaklater.LazyString): 159 | try: 160 | return unicode(o) # For python 2. 161 | except NameError: 162 | return str(o) # For python 3. 163 | except ImportError: 164 | pass 165 | return json.JSONEncoder.default(self, o) 166 | 167 | SERIALIZERS = {"application/json": JSONEncoder(), "text/json": JSONEncoder()} 168 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | setup( 5 | name='Flask-REST', 6 | version='1.4.dev0', 7 | url='http://github.com/ametaireau/flask-rest/', 8 | license='BSD', 9 | author=u"Alexis Métaireau", 10 | author_email="alexis@notmyidea.org", 11 | description="A simple REST toolkit for Flask", 12 | long_description=open('README.rst').read(), 13 | install_requires=['Flask',], 14 | py_modules=['flask_rest'], 15 | classifiers=[ 16 | 'Environment :: Web Environment', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: BSD License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 22 | 'Topic :: Software Development :: Libraries :: Python Modules' 23 | ] 24 | ) 25 | --------------------------------------------------------------------------------