Skip to content

Latest commit

 

History

History
186 lines (131 loc) · 7.55 KB

README.md

File metadata and controls

186 lines (131 loc) · 7.55 KB

django-api-decorators

PyPI version releases commits
dm dw

Tiny decorator functions to make it easier to build an API using Django in ~100 LoC (shorter than this README!)

Table of Contents

I. Installation
II. Usage
III. How it Works
IV. Related Libraries
V. Backstory

Installation

pip install django-api-decorators

It is expected that you already have Django installed

Compatibility

This was originally used in an older Django 1.5 codebase with Python 2.7.

Should work with Django 1.x-2.x and with Python 2.7-3.x

  • Likely works with Django 0.95-0.99 as well, didn't check any earlier versions' release notes
  • 2to3 shows that there is nothing to change, so should be compatible with Python 3.x
  • Have not confirmed if this works with earlier versions of Python.

Please submit a PR or file an issue if you have a compatibility problem or have confirmed compatibility on versions.


Usage

@method_exclusive, per docstring: Checks if request.method is equal to method, if not, returns a 405 not allowed response. Example:

from django_api_decorators import method_exlusive

@method_exclusive('GET')
def get_latest_public_posts(request):
    ...

@require_auth, per docstring: Checks if the request was made by an authenticated user, and if not, returns a 401 unauthorized response. Example:

from django_api_decorators import method_exclusive, require_auth

@method_exclusive('GET')
@require_auth
def get_favorites(request):
    favs = request.user.favorites.all()
    ...

One can add more authorization checks on the User, such as for specific user types, by building on top of the @require_auth decorator. For instance:

from functools import wraps

from django.http import HttpResponse
from django_api_decorators import require_auth

def tenant_exclusive(func):
    """
    Checks if the authenticated user is a tenant, and if not, returns a
    401 unauthorized response
    """
    @wraps(func)
    @require_auth
    def func_wrapper(request, *args, **kwargs):
        if not request.user.is_tenant():
            return HttpResponse(status=401)
        return func(request, *args, **kwargs)
    return func_wrapper

@clean_form, per docstring: Cleans the data in the POST or GET params using the form_class specified. Responds with a 400 bad request if the form is invalid with the errors specified in the form as JSON. Adds the cleaned data as a kwarg (cd) to the decorated function. Example:

from django.shortcuts import get_object_or_404
from django_api_decorators import method_exclusive, clean_form, require_auth

from posts.models import Post
from posts.forms import AddFavForm

@method_exclusive('POST')
@clean_form(AddFavForm)
@require_auth
def add_fav(request, cd):
    post = get_object_or_404(Post, pk=cd['post_id'])
    request.user.favorites.add(post)
    ...

@clean_forms, per docstring: Cleans the data in the POST or GET params using the form_class specified. Responds with a 400 bad request if any of the forms are invalid with the errors specified in the form as JSON. Adds the cleaned data as a kwarg (cd_list) to the decorated function. Example:

from django_api_decorators import method_exclusive, clean_forms, require_auth

from posts.models import Post
from posts.forms import CreatePostForm

@method_exclusive('POST')
@clean_forms(CreatePostForm, 'posts')
@require_auth
def bulk_create_posts(request, cd_list):
    post_list = []
    for data in cd_list:
        post_list.append(Post(
            user=request.user,
            cotent=data['content']
        ))
    Post.objects.bulk_create(post_list)

    ...

How it works

All of the decorators currently just perform a check against the request object, have an early return if the request is invalid, and otherwise let the next function execute. Some of them add a keyword argument when calling the next function so that the interpreted data can be used within it (like with the cleaned dictionaries of forms, which are added as a kwarg of the keyword cd).

I'd encourage you to read the source code, since it's shorter than this README :)

Related Libraries

  • django-serializable-model
    • Django classes to make your models, managers, and querysets serializable, with built-in support for related objects in ~100 LoC

Backstory

This library was built while I was working on Yorango's ad-hoc API and transitioning from an MPA to an SPA. Instead of repeating lots of authentication, authorization, and validation code for every request, I wanted to DRY it up more using decorators or middleware. Decorators would allow us to have early returns with proper HTTP Status Codes for invalid requests. Request code became easier to reason about as a result, guaranteeing it would only execute after authz/authn/etc, and much less prone to accidental bugs, e.g. security issues due to forgetting an authorization check. @method_exclusive, @require_auth, and a few more project-specific decorators were born out of some of those needs.

Validation was a bit more difficult, as we had many existing Django Forms in the MPA, wanted to re-use the classes and validation code we already had in our API instead of re-writing, and wanted to keep things in the same idiomatic style. Django REST Framework has the concept of "Validators", but it is explicitly different from Django's standard Form interface and requires you to buy-in to other parts of DRF to use, like Serializers. @clean_form was born to address those needs. Later @clean_forms was made to address the case of multiple of the same form in one API request (e.g. for bulk creation), somewhat similar to how a Django FormSet might work, but much simpler and requiring a lot less coupling of front and back end code.

These were all used in production with great results, some API methods having just 1 decorator and others having 3 or more decorators, such as:

@method_exclusive('POST')
@clean_form(CreateBillsForm)
@clean_forms(BillForm, 'bills')
@landlord_saas_exclusive
@authorize_action(Listing, 'listing_id')
def create_bills(request, cd, cd_list, listing):
    ...

    # bulk_create the new list of Bills
    bill_list = []
    for data in cd_list:
        bill_list.append(Bill(
            listing=listing,
            price=data['price'],
            due_date=data['due_date'],
        ))
    Bill.objects.bulk_create(bill_list)

    ...

Had been meaning to extract and open source this as well as other various useful utility libraries I had made at Yorango and finally got the chance!