Source code for flask_resources.content_negotiation
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020-2021 CERN.
# Copyright (C) 2020-2021 Northwestern University.
#
# Flask-Resources is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
"""Content negotiation API."""
from functools import wraps
from flask import request
from werkzeug.datastructures import MIMEAccept
from .config import resolve_from_conf
from .context import resource_requestctx
from .errors import MIMETypeNotAccepted
[docs]class ContentNegotiator(object):
"""Content negotiation API.
Implements a procedure for selecting a mimetype best matching what the
client is requesting.
"""
[docs] @classmethod
def match(cls, mimetypes, accept_mimetypes, formats_map, fmt, default=None):
"""Select the MIME type which best matches the client request.
:param mimetypes: Iterable of available MIME types.
:param accept_mimetypes: The client's "Accept" header as MIMEAccept
object.
:param formats_map: Map of format values to MIME type.
:param format: The client's selected format.
:param default: Default MIMEtype if a wildcard was received.
"""
return cls.match_by_format(formats_map, fmt) or cls.match_by_accept(
mimetypes, accept_mimetypes, default=default
)
[docs] @classmethod
def match_by_accept(cls, mimetypes, accept_mimetypes, default=None):
"""Select the MIME type which best matches Accept header.
NOTE: Our match policy differs from Werkzeug's best_match policy:
If the client accepts a specific mimetype and wildcards, and
the server serves that specific mimetype, then favour
that mimetype no matter its quality over the wildcard.
This is as opposed to Werkzeug which only cares about quality.
:param mimetypes: Iterable of available MIME types.
:param accept_mimetypes: The client's "Accept" header as MIMEAccept
object.
:param default: Default MIMEtype if wildcard received.
"""
assert isinstance(accept_mimetypes, MIMEAccept)
assert "*/*" not in mimetypes
# NOTE: accept_mimetypes is already sorted in descending quality order
for client_mimetype, quality in accept_mimetypes:
if client_mimetype in mimetypes:
return client_mimetype
# if here, then no match at all
# WARNING: '*/*' in MIMEAccept object always evaluates to True
# WARNING: MIMEAccept.find('*/*') always evaluates to 0
# So we have to do the following
accepted_values = list(accept_mimetypes.values())
if "*/*" in accepted_values or not accepted_values:
return default
return None
[docs] @classmethod
def match_by_format(cls, formats_map, fmt):
"""Select the MIME type based on a query parameters."""
return formats_map.get(fmt)
[docs]def with_content_negotiation(
response_handlers=None,
default_accept_mimetype=None,
):
"""Decorator to perform content negotiation.
The result of the content negotiation is stored in the resources request
context.
"""
def decorator(f):
@wraps(f)
def inner_content_negotiation(*args, **kwargs):
# Check Accept header i.e. can we even respond to the request in a common
# mimetype?
handlers = resolve_from_conf(response_handlers, resource_requestctx.config)
default_mimetype = resolve_from_conf(
default_accept_mimetype, resource_requestctx.config
)
accept_mimetype = ContentNegotiator.match(
handlers.keys(),
request.accept_mimetypes,
{}, # TODO: Rely on config to populate this formats_map
request.args.get("format", None),
default_mimetype,
)
if not accept_mimetype:
raise MIMETypeNotAccepted(allowed_mimetypes=handlers.keys())
resource_requestctx.accept_mimetype = accept_mimetype
resource_requestctx.response_handler = handlers[accept_mimetype]
return f(*args, **kwargs)
return inner_content_negotiation
return decorator