Login

API_magic API view decorator

Author:
[email protected]
Posted:
June 18, 2008
Language:
Python
Version:
.96
Tags:
view api decorator
Score:
3 (after 3 ratings)

Defines a decorator ``API_magic'' which simplifies input-checking API views.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""
API_magic
Author: Flowgram.com


======== Purpose ========

Defines a decorator ``API_magic'' which simplifies input-checking API views.


======== Parameters to API_magic ========

API_magic takes four parameters
1) The HTTP method (GET or POST) that the API call expects
2) A list ``required_args'' of the names of required arguments. These are then passed to the 
    function as positional arguments, possibly in an altered form. See "Parameter Check and 
    Transformation" below.
3) A dictionary ``permissions'' of permissions the requesting user must possess in order to execute 
    the call on the given parameters. See "Permissions" below.
4) A boolean ``login_required'' indicating whether the requesting user must be authenticated.


======== Return value of decorated function ========

If there is an error in the method, arguments, transformations, or permissions checking, API_magic
will return a response based on the error.
If the API call returns a value (other than None), that value will be returned. 
Finally, the call will return a default 'ok' response.


======== Requirements ========

The following variables must be defined:
- ``response'': A function taking two arguments: a response code and an optional body message, 
    returning an HttpResponse object.

- ``permission_functions'': A dictionary mapping permission names to permission-checking functions. 
    A permission-checking function takes two arguments: a User objects and some other object, and
    returns True if the user has the permission on that object.

- ``permission_responses'': A dictionary mapping permission names to response functions. A response 
    function takes an error message and returns an HttpResponse object.

- ``transformations'': A dictionary mapping request argument names to 
    (type_name, converter_function) tuples. A converter_function is a function that takes the value 
    of the request argument (a string) and returns an object of type type_name.
    

======== Example usage ======== 

# code before using API_magic
def some_api_function(request):
    if request.method != 'POST':
        return response('method-post')
    if not 'foobar_id' in request.POST.keys():
        return response('input-missing', 'foobar_id is a required POST arg')
    try:
        foobar = Foobar.objects.get(id=request.POST['foobar_id'])
    except Foobar.DoesNotExist:
        msg = "foobar_id %s could not be converted to Foobar object" % foobar_id
        return response('input-invalid', msg)
    if not permissions.can_edit(foobar, request.user):
        msg = "%s does not have permission to edit that foobar_id" % request.user
        return response('permission-edit', msg)
    if not 'title' in request.POST.keys():
        return response('input-missing', 'title is a required POST arg')
    ...do work here...
    return response('Done')

# code after using API_magic
@API_magic('POST', ['foobar_id', 'title'], {'foobar_id': 'edit'})
def some_api_function(request, foobar, title):
    ...do work here...
    return response('Done')
    

======== Parameter Check and Transformation ======== 

A function decorated with API_magic validates the presence of required arguments named in the
second parameter. For the example just above, it would make sure that there are arguments named
'foobar_id' and 'title' in request.POST, returning an API error response otherwise. Of course,
optional parameters can still be used, you just don't include them in the required arguments list.

Next, transformations are applied based on the argument's name. Transformations generally convert
the argument from strings to another type. This is probably the most magical thing going on.

There is a dictionary in this module called 'transformations' which maps arg-names to the
transformation to be applied. Any explicitly required function argument with a name among the 
transformations will be transformed before it reaches the main function code. The keys in the
'transformations' dictionary are (typename, convert function) pairs. The typename is a string
to be used in the API error message that is returned if the conversion fails. 

For the example above, the 'foobar_id' argument would be transformed to a Foobar object before 
it is passed to the function.


======== Permissions ========

The third argument to API_magic is an optional dictionary of permissions to be checked. The keys
are strings that match required-argument names, and the values are strings that are keys in both
the permission_functions and permission_responses dictionaries locally defined. The values of the
permission_functions dictionary are permission-checking functions which return a boolean value.
The argument is transformed *before* it is passed to the permission checker -- for the example
above, the 'edit' permission checker is passed a Foobar object, not a foobar_id (string). If
the permission check fails (returns false), the decorated function will return an error 
response defined by the permission_responses function for the given permission -- these functions
take a string error messsage as an argument.
"""

from functools import wraps

from django.contrib.auth.models import User

def response(code, msg=''):
    """Simple example response generator"""
    from django.utils import simplejson
    r = {'code': code,
         'succeeded': 1 if code=='ok' else 0,
         'body': msg
         }
    return HttpResponse(simplejson.dumps(r), mimetype='application/json')

# you need to make your own functions here, obviously:
from core import permissions 
permission_functions = {
        'view' : permissions.can_view,
        'edit' : permissions.can_edit,
    }

permission_responses = {
        'view' : lambda x: response('permission-view', x),
        'edit' : lambda x: response('permission-edit', x)
    } 

def fetch_foobar(foobarid):
    from core.models import Foobar
    return Foobar.objects.get(id=foobarid)

transformations = {
       # arg name         # Type name     # converter function
        'foobar_id'     : ('Foobar',        fetch_foobar),
        'time'          : ('integer',       int),
    }

def API_magic(method='POST', required_args=[], permissions={}, login_required=False):
    def check_args(func):
        # First, validate arguments *to API_magic* and raise an exception if they are bad. 
        # This happens when the code is first loaded by the server.
        #
        #  validate the method:
        if method not in ['GET', 'POST']: # add more here if you want
            err_msg = "Invalid method %s passed to API_magic by %s" % (method, func.__name__)
            raise ValueError(err_msg)
        #  validate the permissions:
        for arg_name, perm_name in permissions.items():
            if arg_name not in required_args:
                err_msg = "Permission on unknown arg %s passed to API_magic by %s" % (arg_name, func.__name__)
                raise ValueError(err_msg)
            if perm_name not in permission_functions.keys():
                err_msg = "Permission %s with unknown checker function passed to API_magic by %s"\
                    % (perm_name, func.__name__)
                raise ValueError(err_msg)
            if perm_name not in permission_responses.keys():
                err_msg = "Permission %s with unknown response passed to API_magic by %s" % (perm_name, func.__name__)
                raise ValueError(err_msg)

        # Next, create an input-validating version of the decorated function:
        @wraps(func)
        def new_f(request, *args, **kwargs):
            # Validate authentication
            if login_required:
                if not request.user.is_authenticated():
                    return response('login')

            # Validate call method and get the arguments
            if method == 'GET':
                if request.method != 'GET':
                    return response('method-get')
                arg_box = request.GET
            else: # method == 'POST':
                if request.method != 'POST':
                    return response('method-post')
                arg_box = request.POST

            # For each required argument:
            for name in required_args:
                # Make sure it's present
                if not name in arg_box.keys():
                    return response('input-missing', "%s is a required %s argument" % (name, method))
                value = arg_box[name]
                # Apply any transformation
                if name in transformations.keys():
                    type_name, converter = transformations[name]
                    try:
                        value = converter(value)
                    except:
                        err_msg = "%s %s could not be converted to %s object" % (name, value, type_name)
                        return response('input-invalid', err_msg)
                # Check permissions on it
                if name in permissions.keys():
                    permission_name = permissions[name]
                    perm_checker = permission_functions[permission_name]
                    perm_responder = permission_responses[permission_name]
                    if not perm_checker(request.user, value):
                        err_msg = "%s does not have permission to %s that %s" % (request.user, permission_name, name)
                        return perm_responder(err_msg)
                # Add the (possibly transformed) value to the fixed function args
                args += (value,)

            # Call function with new arguments and return the function's return, or a default 'ok'
            r = func(request, *args, **kwargs)
            return r if r else response('ok')
        return new_f
    return check_args

More like this

Comments

Please login first before commenting.