2

I'm building a rate-limiting decorator in flask using redis stores that will recognize different limits on different endpoints. (I realize there are a number of rate-limiting decorators out there, but my use case is different enough that it made sense to roll my own.)

Basically the issue I'm having is ensuring that the keys I store in redis are class-specific. I'm using the blueprint pattern in flask, which basically works like this:

class SomeEndpoint(MethodView):
    def get(self):
        # Respond to get request
    def post(self):
        # Respond to post request

The issue here is that I want to be able to rate limit the post method of these classes without adding any additional naming conventions. In my mind the best way to do this would be something like this:

class SomeEndpoint(MethodView):

    @RateLimit  # Access SomeEndpoint class name
    def post(self):
        # Some response

but within the decorator, only the post function is in scope. How would I get back to the SomeEndpoint class given the post function? This is the basic layout of the decorator. That might be confusing, so here's a more concrete example of the decorator.

class RateLimit(object):
"""
The base decorator for app-specific rate-limiting.
"""
def __call__(self, f):
    def endpoint(*args, **kwargs):
        print class_backtrack(f)  # Should print SomeEnpoint
        return f(*args, **kwargs)
    return endpoint

basically looking for what that class_backtrack function looks like. I've looked through the inspect module, but I haven't found anything that seems to accomplish this.

Slater Victoroff
  • 21,376
  • 21
  • 85
  • 144
  • Just to confirm: Do you need the class that defines the method, rather than the class of the object? – user2357112 Jan 30 '14 at 21:07
  • @user2357112 That is in fact correct. – Slater Victoroff Jan 30 '14 at 21:08
  • I'm pretty sure this is possible - Python 3's `super` does something almost the same - but I don't know how to go about it. – user2357112 Jan 30 '14 at 21:09
  • @user2357112 Thank you for the vote of confidence. Do you know of anything worth rooting around in other than `insepct`? – Slater Victoroff Jan 30 '14 at 21:10
  • ...crap. `super` uses [compile-time magic](http://stackoverflow.com/questions/19608134/why-is-python-3-xs-super-magic) that only triggers when a method uses the name `super` inside it. It's not something you can replicate with `inspect`. – user2357112 Jan 30 '14 at 21:14
  • @user2357112 That... is unfortunate... I feel like this use case isn't that weird. Is there a better way to go about this? I would rather not have to explicitly pass a key to the decorator every time. – Slater Victoroff Jan 30 '14 at 21:16
  • If you only need the class's name, Python 3 would let you use the function's `__qualname__` to determine it, but you're not on Python 3. – user2357112 Jan 30 '14 at 21:18
  • @user2357112 I guess I could go through the `__module__` attribute and just look for a class with a matching function... If I didn't need `boto` I would probably just switch to python3. – Slater Victoroff Jan 30 '14 at 21:22
  • I'm not sure it will be possible to find the class that _defines_ the method in P2.7. The function being wrapped is still not converted into an unbound method at the point of decoration, so it holds no reference to its class yet, and at call time all you have is the instance which might be a subclass. You might be able to decorate the entire _class_ and replace methods of interest at that point? – lanzz Jan 30 '14 at 21:34

1 Answers1

0

You can decorate the entire class instead of just the methods:

def wrap(Class, method):
    def wrapper(self, *args, **kwargs):
        print Class
        return method(self, *args, **kwargs)
    return method.__class__(wrapper, None, Class)

def rate_limit(*methods):
    def decorator(Class):
        for method_name in methods:
            method = getattr(Class, method_name)
            setattr(Class, method_name, wrap(Class, method))
        return Class
    return decorator

@rate_limit('post')
class SomeEndpoint(object):

    def post(self):
        pass

class Subclass(SomeEndpoint):
    pass

a = Subclass()
a.post()
# prints <class 'SomeEndpoint'>
lanzz
  • 42,060
  • 10
  • 89
  • 98
  • If someone doesn't find a better alternative within a day or so I'll accept this. It works, but gets relatively unwieldy when adding arguments to the decorator. – Slater Victoroff Jan 30 '14 at 22:53