Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Route requests based on the Accept header in Flask

I would like to route to a different Flask views based on the Accept HTTP header, for example:

@api.route('/test', accept='text/html')
def test_html():
    return "<html><body>Test</body></html>"

@api.route('/test', accept='text/json')
def test_json():
    return jsonify(test="Test")

I haven't found relevant option in Werkzeug Rule constructor, which is used by Flask. Is it a missing feature or is it possible to achieve the same effect differently, for example by intercepting and modifying URL path before routing?

I don't want to merge the views into one because it would complicate code significantly, there are many of them and they reside in different blueprints.

I am aware that similar question has been asked, but nobody answered it using Flask. It's possible to do it in different web frameworks, for example in Pyramid using predicates - sample code can be found in this answer.

like image 492
loomchild Avatar asked Mar 01 '15 08:03

loomchild


1 Answers

I wrote a decorator which does that (copying here for posterity). It's just a rough idea that could be improved further (e.g. returning 406 Not Acceptable response instead of using the default handler when there are no handlers that match given MIME type). More explanations are in the comments.

import functools
from flask import Flask, request, jsonify

app = Flask(__name__)

def accept(func_or_mimetype=None):
    """Decorator which allows to use multiple MIME type handlers for a single
    endpoint.
    """

    # Default MIME type.
    mimetype = 'text/html'

    class Accept(object):
        def __init__(self, func):
            self.default_mimetype = mimetype
            self.accept_handlers = {mimetype: func}
            functools.update_wrapper(self, func)

        def __call__(self, *args, **kwargs):
            default = self.default_mimetype
            mimetypes = request.accept_mimetypes
            best = mimetypes.best_match(self.accept_handlers.keys(), default)
            # In case of Accept: */*, choose default handler.
            if best != default and mimetypes[best] == mimetypes[default]:
                best = default
            return self.accept_handlers[best](*args, **kwargs)

        def accept(self, mimetype):
            """Register a MIME type handler."""

            def decorator(func):
                self.accept_handlers[mimetype] = func
                return func
            return decorator

    # If decorator is called without argument list, return Accept instance.
    if callable(func_or_mimetype):
        return Accept(func_or_mimetype)

    # Otherwise set new MIME type (if provided) and let Accept act as a
    # decorator.
    if func_or_mimetype is not None:
        mimetype = func_or_mimetype
    return Accept

@app.route('/')
@accept     # Or: @accept('text/html')
def index():
    return '<strong>foobar</strong>'

@index.accept('application/json')
def index_json():
    return jsonify(foobar=True)

@index.accept('text/plain')
def index_text():
    return 'foobar\n', 200, {'Content-Type': 'text/plain'}
like image 157
Audrius Kažukauskas Avatar answered Oct 05 '22 23:10

Audrius Kažukauskas