Jesper Nøhr avatar Jesper Nøhr committed 788bbd3

consumer management, documentation generation, api views

Comments (0)

Files changed (6)

piston/authentication.py

-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseRedirect
 from django.contrib.auth.models import User
 from django.contrib.auth.decorators import login_required
 from django.template import loader
 from django.conf import settings
+from django.core.urlresolvers import get_callable
 
 import oauth
 from store import DataStore
     return response
 
 def oauth_auth_view(request, token, callback, params):
-    return HttpResponse("Just a fake view for auth.")
+    return HttpResponse("Just a fake view for auth. %s, %s, %s" % (token, callback, params))
 
 @login_required
 def oauth_user_auth(request):
         
     try:
         token = oauth_server.fetch_request_token(oauth_request)
-    except oath.OAuthError, err:
+    except oauth.OAuthError, err:
         return send_oauth_error(err)
         
     try:
     if request.method == "GET":
         request.session['oauth'] = token.key
         params = oauth_request.get_normalized_parameters()
-        return oauth_auth_view(request, token, callback, params)
+        oauth_view = getattr(settings, 'OAUTH_AUTH_VIEW', 'oauth_auth_view')
+        return get_callable(oauth_view)(request, token, callback, params)
     elif request.method == "POST":
         if request.session.get('oauth', '') == token.key:
             request.session['oauth'] = ''
             
             try:
-                if int(request.POST.get('authorize_access')):
+                if int(request.POST.get('authorize_access', '0')):
                     token = oauth_server.authorize_token(token, request.user)
-                    args = token.to_string(only_key=True)
+                    args = '?'+token.to_string(only_key=True)
                 else:
-                    args = 'error=%s' % 'Access not granted by user.'
+                    args = '?error=%s' % 'Access not granted by user.'
                 
-                response = HttpResponse('Fake callback.')
+                if not callback:
+                    callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
+                    return get_callable(callback)(request, token)
                     
-            except OAuthError, err:
+                response = HttpResponseRedirect(callback+args)
+                    
+            except oauth.OAuthError, err:
                 response = send_oauth_error(err)
         else:
             response = HttpResponse('Action not allowed.')
 
             if consumer and token:
                 request.user = token.user
+                request.throttle_extra = token.consumer.id
                 return True
             
         return False

piston/decorator.py

+"""
+Decorator module, see
+http://www.phyast.pitt.edu/~micheles/python/documentation.html
+for the documentation and below for the licence.
+"""
+
+## The basic trick is to generate the source code for the decorated function
+## with the right signature and to evaluate it.
+## Uncomment the statement 'print >> sys.stderr, func_src'  in _decorator
+## to understand what is going on.
+
+__all__ = ["decorator", "new_wrapper", "getinfo"]
+
+import inspect, sys
+
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+def getinfo(func):
+    """
+    Returns an info dictionary containing:
+    - name (the name of the function : str)
+    - argnames (the names of the arguments : list)
+    - defaults (the values of the default arguments : tuple)
+    - signature (the signature : str)
+    - doc (the docstring : str)
+    - module (the module name : str)
+    - dict (the function __dict__ : str)
+    
+    >>> def f(self, x=1, y=2, *args, **kw): pass
+
+    >>> info = getinfo(f)
+
+    >>> info["name"]
+    'f'
+    >>> info["argnames"]
+    ['self', 'x', 'y', 'args', 'kw']
+    
+    >>> info["defaults"]
+    (1, 2)
+
+    >>> info["signature"]
+    'self, x, y, *args, **kw'
+    """
+    assert inspect.ismethod(func) or inspect.isfunction(func)
+    regargs, varargs, varkwargs, defaults = inspect.getargspec(func)
+    argnames = list(regargs)
+    if varargs:
+        argnames.append(varargs)
+    if varkwargs:
+        argnames.append(varkwargs)
+    signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults,
+                                      formatvalue=lambda value: "")[1:-1]
+    return dict(name=func.__name__, argnames=argnames, signature=signature,
+                defaults = func.func_defaults, doc=func.__doc__,
+                module=func.__module__, dict=func.__dict__,
+                globals=func.func_globals, closure=func.func_closure)
+
+# akin to functools.update_wrapper
+def update_wrapper(wrapper, model, infodict=None):
+    infodict = infodict or getinfo(model)
+    try:
+        wrapper.__name__ = infodict['name']
+    except: # Python version < 2.4
+        pass
+    wrapper.__doc__ = infodict['doc']
+    wrapper.__module__ = infodict['module']
+    wrapper.__dict__.update(infodict['dict'])
+    wrapper.func_defaults = infodict['defaults']
+    wrapper.undecorated = model
+    return wrapper
+
+def new_wrapper(wrapper, model):
+    """
+    An improvement over functools.update_wrapper. The wrapper is a generic
+    callable object. It works by generating a copy of the wrapper with the 
+    right signature and by updating the copy, not the original.
+    Moreovoer, 'model' can be a dictionary with keys 'name', 'doc', 'module',
+    'dict', 'defaults'.
+    """
+    if isinstance(model, dict):
+        infodict = model
+    else: # assume model is a function
+        infodict = getinfo(model)
+    assert not '_wrapper_' in infodict["argnames"], (
+        '"_wrapper_" is a reserved argument name!')
+    src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict
+    funcopy = eval(src, dict(_wrapper_=wrapper))
+    return update_wrapper(funcopy, model, infodict)
+
+# helper used in decorator_factory
+def __call__(self, func):
+    infodict = getinfo(func)
+    for name in ('_func_', '_self_'):
+        assert not name in infodict["argnames"], (
+           '%s is a reserved argument name!' % name)
+    src = "lambda %(signature)s: _self_.call(_func_, %(signature)s)"
+    new = eval(src % infodict, dict(_func_=func, _self_=self))
+    return update_wrapper(new, func, infodict)
+
+def decorator_factory(cls):
+    """
+    Take a class with a ``.caller`` method and return a callable decorator
+    object. It works by adding a suitable __call__ method to the class;
+    it raises a TypeError if the class already has a nontrivial __call__
+    method.
+    """
+    attrs = set(dir(cls))
+    if '__call__' in attrs:
+        raise TypeError('You cannot decorate a class with a nontrivial '
+                        '__call__ method')
+    if 'call' not in attrs:
+        raise TypeError('You cannot decorate a class without a '
+                        '.call method')
+    cls.__call__ = __call__
+    return cls
+
+def decorator(caller):
+    """
+    General purpose decorator factory: takes a caller function as
+    input and returns a decorator with the same attributes.
+    A caller function is any function like this::
+
+     def caller(func, *args, **kw):
+         # do something
+         return func(*args, **kw)
+    
+    Here is an example of usage:
+
+    >>> @decorator
+    ... def chatty(f, *args, **kw):
+    ...     print "Calling %r" % f.__name__
+    ...     return f(*args, **kw)
+
+    >>> chatty.__name__
+    'chatty'
+    
+    >>> @chatty
+    ... def f(): pass
+    ...
+    >>> f()
+    Calling 'f'
+
+    decorator can also take in input a class with a .caller method; in this
+    case it converts the class into a factory of callable decorator objects.
+    See the documentation for an example.
+    """
+    if inspect.isclass(caller):
+        return decorator_factory(caller)
+    def _decorator(func): # the real meat is here
+        infodict = getinfo(func)
+        argnames = infodict['argnames']
+        assert not ('_call_' in argnames or '_func_' in argnames), (
+            'You cannot use _call_ or _func_ as argument names!')
+        src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict
+        # import sys; print >> sys.stderr, src # for debugging purposes
+        dec_func = eval(src, dict(_func_=func, _call_=caller))
+        return update_wrapper(dec_func, func, infodict)
+    return update_wrapper(_decorator, caller)
+
+if __name__ == "__main__":
+    import doctest; doctest.testmod()
+
+##########################     LEGALESE    ###############################
+      
+##   Redistributions of source code must retain the above copyright 
+##   notice, this list of conditions and the following disclaimer.
+##   Redistributions in bytecode form must reproduce the above copyright
+##   notice, this list of conditions and the following disclaimer in
+##   the documentation and/or other materials provided with the
+##   distribution. 
+
+##   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+##   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+##   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+##   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+##   HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+##   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+##   BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+##   OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+##   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+##   TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+##   USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+##   DAMAGE.
+import inspect, handler
+
+def generate_doc(handler_cls):
+    """
+    Returns a `HandlerDocumentation` object
+    for the given handler. Use this to generate
+    documentation for your API.
+    """
+    if not type(handler_cls) is handler.HandlerMetaClass:
+        raise ValueError("Give me handler, not %s" % type(handler_cls))
+        
+    return HandlerDocumentation(handler_cls)
+    
+class HandlerMethod(object):
+    def __init__(self, method, stale=False):
+        self.method = method
+        self.stale = stale
+        
+    def iter_args(self):
+        args, _, _, defaults = inspect.getargspec(self.method)
+
+        for idx, arg in enumerate(args):
+            if arg in ('self', 'request'):
+                continue
+
+            didx = len(args)-idx
+
+            if defaults and len(defaults) >= didx:
+                yield (arg, str(defaults[-didx]))
+            else:
+                yield (arg, None)
+        
+    def get_signature(self, parse_optional=True):
+        spec = ""
+
+        for argn, argdef in self.iter_args():
+            spec += argn
+            
+            if argdef:
+                spec += '=%s' % argdef
+            
+            spec += ', '
+            
+        spec = spec.rstrip(", ")
+        
+        if parse_optional:
+            return spec.replace("=None", "=<optional>")
+            
+        return spec
+
+    signature = property(get_signature)
+        
+    def get_doc(self):
+        return inspect.getdoc(self.method)
+    
+    doc = property(get_doc)
+    
+    def get_name(self):
+        return self.method.__name__
+        
+    name = property(get_name)
+    
+    def __repr__(self):
+        return "<Method: %s>" % self.name
+    
+class HandlerDocumentation(object):
+    def __init__(self, handler):
+        self.handler = handler
+        
+    def get_methods(self, include_default=False):
+        for method in "read create update delete".split():
+            met = getattr(self.handler, method)
+            stale = inspect.getmodule(met) is handler
+
+            if met and (not stale or include_default):
+                yield HandlerMethod(met, stale)
+        
+    @property
+    def is_anonymous(self):
+        return False
+
+    @property
+    def model(self):
+        return getattr(self, 'model', None)
+    
+    @property
+    def name(self):
+        return self.handler.__name__
+    
+    def __repr__(self):
+        return u'<Documentation for "%s">' % self.name

piston/managers.py

 SECRET_SIZE = 16
 
 class ConsumerManager(models.Manager):
-    def create_consumer(self, name, user=None):
-        """Shortcut to create a consumer with random key/secret."""
+    def create_consumer(self, name, description=None, user=None):
+        """
+        Shortcut to create a consumer with random key/secret.
+        """
         consumer, created = self.get_or_create(name=name)
-        if user is not None:
+
+        if user:
             consumer.user = user
+
+        if description:
+            consumer.description = description
+
         if created:
             consumer.generate_random_codes()
+
         return consumer
     
     _default_consumer = None
-    def get_default_consumer(self, name):
-        """Add cache if you use a default consumer."""
-        if self._default_consumer is None:
-            self._default_consumer = self.get(name=name)
-        return self._default_consumer
-        
 
 class ResourceManager(models.Manager):
     _default_resource = None
+
     def get_default_resource(self, name):
-        """Add cache if you use a default resource."""
-        if self._default_resource is None:
+        """
+        Add cache if you use a default resource.
+        """
+        if not self._default_resource:
             self._default_resource = self.get(name=name)
-        return self._default_resource
-        
+
+        return self._default_resource        
 
 class TokenManager(models.Manager):
     def create_token(self, consumer, token_type, timestamp, user=None):
-        """Shortcut to create a token with random key/secret."""
+        """
+        Shortcut to create a token with random key/secret.
+        """
         token, created = self.get_or_create(consumer=consumer, 
                                             token_type=token_type, 
                                             timestamp=timestamp,
                                             user=user)
+
         if created:
             token.generate_random_codes()
+
         return token
 from django.db import models
 from django.contrib.auth.models import User
 from django.contrib import admin
+from django.conf import settings
+from django.core.mail import send_mail, mail_admins
+from django.template import loader
 
 from managers import TokenManager, ConsumerManager, ResourceManager
 
-KEY_SIZE = 16
-SECRET_SIZE = 16
+KEY_SIZE = 18
+SECRET_SIZE = 32
+
+CONSUMER_STATES = (
+    ('pending', 'Pending approval'),
+    ('accepted', 'Accepted'),
+    ('canceled', 'Canceled'),
+)
 
 class Nonce(models.Model):
     token_key = models.CharField(max_length=KEY_SIZE)
 
 class Consumer(models.Model):
     name = models.CharField(max_length=255)
+    description = models.TextField()
+
     key = models.CharField(max_length=KEY_SIZE)
     secret = models.CharField(max_length=SECRET_SIZE)
-    
-    user = models.ForeignKey(User, null=True, blank=True)
+
+    status = models.CharField(max_length=16, choices=CONSUMER_STATES, default='pending')
+    user = models.ForeignKey(User, null=True, blank=True, related_name='consumers')
 
     objects = ConsumerManager()
         
         key = User.objects.make_random_password(length=KEY_SIZE)
 
         secret = User.objects.make_random_password(length=SECRET_SIZE)
+
         while Consumer.objects.filter(key__exact=key, secret__exact=secret).count():
             secret = User.objects.make_random_password(length=SECRET_SIZE)
 
         self.secret = secret
         self.save()
 
+    # -- 
+    
+    def save(self, **kwargs):
+        super(Consumer, self).save(**kwargs)
+        
+        if self.id and self.user:
+            subject = "Bitbucket API Consumer"
+            rcpt = [ self.user.email, ]
+
+            if self.status == "accepted":
+                template = "api/mails/consumer_accepted.txt"
+                subject += " was accepted!"
+            elif self.status == "canceled":
+                template = "api/mails/consumer_canceled.txt"
+                subject += " has been canceled"
+            else:
+                template = "api/mails/consumer_pending.txt"
+                subject += " application received"
+                
+                for admin in settings.ADMINS:
+                    bcc.append(admin[1])
+
+            body = loader.render_to_string(template, 
+                    { 'consumer': self, 'user': self.user })
+                    
+            send_mail(subject, body, 'api-noreply@bitbucket.org', 
+                        rcpt, fail_silently=True)
+            
+            if self.status == 'pending':
+                mail_admins(subject, body, fail_silently=True)
+                        
+            if settings.DEBUG:
+                print "Mail being sent, to=%s" % rcpt
+                print "Subject: %s" % subject
+                print body
+
 admin.site.register(Consumer)
 
 class Token(models.Model):
+from functools import wraps
 from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse
 from django.core.urlresolvers import reverse
+from django.core.cache import cache
+from decorator import decorator
+
+from datetime import datetime, timedelta
 
 def create_reply(message, status=200):
     return HttpResponse(message, status=status)
     DUPLICATE_ENTRY = create_reply('Conflict/Duplicate', status=409)
     NOT_HERE = create_reply('Gone', status=410)
     NOT_IMPLEMENTED = create_reply('Not Implemented', status=501)
+    THROTTLED = create_reply('Throttled', status=503)
     
 class FormValidationError(Exception):
     def __init__(self, form):
         self.code = code
 
 def validate(v_form, operation='POST'):
+    @decorator
     def dec(func):
         def wrap(self, request, *a, **kwa):
             form = v_form(getattr(request, operation))
         return wrap
     return dec
 
+def throttle(max_requests, timeout=60*60):
+    """
+    Simple throttling decorator, caches
+    the amount of requests made in cache.
+    
+    If used on a view where users are required to
+    log in, the username is used, otherwise the
+    IP address of the originating request is used.
+    
+    Parameters::
+     - `max_requests`: The maximum number of requests
+     - `timeout`: The timeout for the cache entry (default: 1 hour)
+    """
+    @decorator
+    def inner(f):
+        def wrap(self, request, *args, **kwargs):
+            if request.user.is_authenticated():
+                ident = request.user.username
+            else:
+                ident = request.META.get('REMOTE_ADDR', None)
+
+            if hasattr(request, 'throttle_extra'):
+                """
+                Since we want to be able to throttle on a per-
+                application basis, it's important that we realize
+                that `throttle_extra` might be set on the request
+                object. If so, append the identifier name with it.
+                """
+                ident += ':%s' % str(request.throttle_extra)
+            
+            if ident:
+                """
+                Preferrably we'd use incr/decr here, since they're
+                atomic in memcached, but it's in django-trunk so we
+                can't use it yet. If someone sees this after it's in
+                stable, you can change it here.
+                """
+                now = datetime.now()
+                ts_key = 'throttle:ts:%s' % ident
+                timestamp = cache.get(ts_key)
+                offset = now + timedelta(seconds=timeout)
+
+                if timestamp and timestamp < offset:
+                    t = rc.THROTTLED
+                    wait = timeout - (offset-timestamp).seconds
+                    t.content = 'Throttled, wait %d seconds.' % wait
+                    
+                    return t
+                    
+                count = cache.get(ident, 1)
+                cache.set(ident, count+1)
+                
+                if count >= max_requests:
+                    cache.set(ts_key, offset, timeout)
+                    cache.set(ident, 1)
+
+            return f(self, request, *args, **kwargs)
+        return wrap
+    return inner
+
 def coerce_put_post(request):
     if request.method == "PUT":
         request.method = "POST"
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.