Negotiator2
===========

.. image:: https://travis-ci.org/zimeon/negotiator2.svg?branch=master
    :target: https://travis-ci.org/zimeon/negotiator2
.. image:: https://coveralls.io/repos/github/zimeon/negotiator2/badge.svg?branch=master
    :target: https://coveralls.io/github/zimeon/negotiator2?branch=master

Framework neutral HTTP Content and Datetime Negotiation for Python

Introduction
------------

Negotiator2 offers a framework for making content and datetime negotiation decisions based on the HTTP accept headers. This is an updated fork of the `Negotiator 1.0.0
<https://pypi.python.org/pypi/negotiator/1.0.0>`_ module written by Richard Jones and last updated 2012-03-14.
Changes from that code are:

  * Support for python 2.6, 2.7, and python 3.4 and up
  * Converted examples into tests
  * Added utility function `conneg_on_accept`
  * Added datetime negotiaton and utility function `negotiate_on_datetime`

Content Negotiation
===================

Negotiator2 supports content negotiation only on the ``Accept`` and ``Accept-Language`` headers. It does not support ``Accept-Charset`` and ``Accept-Encoding``.

Utility Function
----------------

A single function call that takes the formats supported by the server and the ``Accept`` header from the client to return the best format to respond with:

    >>> from negotiator2 import conneg_on_accept
    >>> conneg_on_accept(["text/html","application/json"], "text/plain, text/html;q=0.5")
    'text/html'
    >>> conneg_on_accept(["text/html","application/json"], "application/json, application/ld+json")
    'application/json'

Basic Usage
-----------

Import all the content negotiation objects from the negotiator2 module

    >>> from negotiator2 import ContentNegotiator, AcceptParameters, ContentType, Language

Specify the default parameters.  These are the parameters which will be used in place of any HTTP Accept headers which are not present in the negotiation request.  For example, if the Accept-Language header is not passed to the negotiator it will assume that the client request is for "en"

    >>> default_params = AcceptParameters(ContentType("text/html"), Language("en"))

Specify the list of acceptable formats that the server supports

    >>> acceptable = [AcceptParameters(ContentType("text/html"), Language("en"))]
    >>> acceptable.append(AcceptParameters(ContentType("text/json"), Language("en")))

Create an instance of the negotiator, ready to accept negotiation requests

    >>> cn = ContentNegotiator(default_params, acceptable)

A simple negotiate on the HTTP Accept header "text/json;q=1.0, text/html;q=0.9", asking for ``json``, and if not ``json`` then ``html``

    >>> acceptable = cn.negotiate(accept="text/json;q=1.0, text/html;q=0.9")

The negotiator indicates that the best match the server can give to the client's request is ``text/json`` in English

    >>> acceptable
    AcceptParameters:: Content Type: text/json;Language: en;


Advanced Usage
--------------

Import all the content negotiation objects from the negotiator2 module

    >>> from negotiator2 import ContentNegotiator, AcceptParameters, ContentType, Language

Specify the default parameters.  These are the parameters which will be used in place of any HTTP ``Accept`` headers which are not present in the negotiation request. For example, if the ``Accept-Language`` header is not passed to the negotiator it will assume that the client request is for "en"

    >>> default_params = AcceptParameters(ContentType("text/html"), Language("en"))

Specify the list of acceptable formats that the server supports.  For this advanced example we specify ``html``, ``json`` and ``pdf`` in a variety of languages

    >>> acceptable = [AcceptParameters(ContentType("text/html"), Language("en"))]
    >>> acceptable.append(AcceptParameters(ContentType("text/html"), Language("fr")))
    >>> acceptable.append(AcceptParameters(ContentType("text/html"), Language("de")))
    >>> acceptable.append(AcceptParameters(ContentType("text/json"), Language("en")))
    >>> acceptable.append(AcceptParameters(ContentType("text/json"), Language("cz")))
    >>> acceptable.append(AcceptParameters(ContentType("application/pdf"), Language("de")))

specify the weighting that the negotiator should apply to the different ``Accept`` headers.  A higher weighting towards content type will prefer content type variations over language variations (e.g. if there are two formats which are equally acceptable to the client, in different languages, a ``content_type`` weight higher than a language weight will return the parameters according to the server's preferred content type.

    >>> weights = {"content_type" : 1.0, "language" : 0.5}

Create an instance of the negotiator, ready to accept negotiation requests

    >>> cn = ContentNegotiator(default_params, acceptable, weights)

set up some more complex accept headers (you can try modifying the order of the elements without ``q`` values, and the ``q`` values themselves, to see different results).

    >>> accept = "text/html, text/json;q=1.0, application/pdf;q=0.5"
    >>> accept_language = "en;q=0.5, de, cz, fr"

negotiate over both headers, looking for an optimal solution to the client request

    >>> acceptable = cn.negotiate(accept, accept_language)

The negotiator indicates the best fit to the client request is ``text/html`` in German

    >>> acceptable
    AcceptParameters:: Content Type: text/html;Language: de;


Preference Ordering Rules
-------------------------

Negotiator2 content negotiation organises the preferences in each accept header into a sequence,
from highest ``q`` value to lowest, grouping together equal ``q`` values.

For example, the HTTP ``Accept`` header:

    "text/html, text/json;q=1.0, application/pdf;q=0.5"
    
Would result in the following preference sequence (as a python dictionary):

    {
        1.0 : ["text/json", "text/html"],
        0.5 : ["application/pdf"]
    }

While the HTTP ``Accept-Language`` header:

    "en;q=0.5, de, cz, fr"
    
Would result in the following preference sequence (as a python dictionary):

    {
        1.0 : ["de"],
        0.8 : ["cz"],
        0.6 : ["fr"],
        0.5 : ["en"]
    }

(In reality, the ``q`` values for ``de``, ``cz`` and ``fr`` would be evenly spaced between 1.0 and 0.5, using floating point numbers as the keys)


Combined Preference Ordering Rules
----------------------------------

The negotiator will compute all the possible allowed combinations and their weighted overall ``q`` values.

Given that the server supports the following combinations (from the code example above):

    text/html, en
    text/html, fr
    text/html, de
    text/json, en
    text/json, cz
    application/pdf, de

And given the weights:

    w = {"content_type" : 1.0, "language" : 0.5}

We can calculate the combined ``q`` value of each allowed (by both server and client) option, using the equation:

    overall_q = w["content_type"] * content_type_q + w["language"] * language_q
    
So, for the above options and ``q`` values from the previous section, we can generate the preference list (as a python dictionary):

    {
        1.5  : ["text/html, de"],
        1.4  : ["text/json, cz"],
        1.3  : ["text/html, fr"],
        1.25 : ["text/html, en", "text/json, en"]
        1.0  : ["application/pdf, de"]
    }

It is clear, then, why the negotiator in the Advanced Usage section selected ``"text/html, de"`` as its preferred format.

Datetime Negotiation
====================

Negotiator2 implements datetime negotiation as described in `RFC7089: HTTP Framework for Time-Based Access to Resource States -- Memento
<https://tools.ietf.org/html/rfc7089>`_

Simple Datetime Negotiation
---------------------------

The simplest way to use negotiator2 to select from a an original reource and a set of mementos is to first build the ``TimeMap`` describing the versions, and to then call ``negotiate_on_datetime`` to select the best version for a given datetime (specified in the form of the ``Accept-Datetime`` HTTP header).

    >>> from negotiator2 import TimeMap, negotiate_on_datetime
    >>> tm = TimeMap()
    >>> tm.set_original("http://example.org/R", "Thu, 02 Nov 2017 16:29:00 GMT")
    >>> tm.add_memento("http://example.org/M1", "Thu, 02 Nov 2017 10:00:00 GMT")
    >>> tm.add_memento("http://example.org/M2", "Wed, 01 Nov 2017 09:00:00 GMT")
    >>> negotiate_on_datetime(tm, "Thu, 02 Nov 2017 17:00:00 GMT")
    'http://example.org/R'
    >>> negotiate_on_datetime(tm, "Thu, 02 Nov 2017 11:00:00 GMT")
    'http://example.org/M1'
    >>> negotiate_on_datetime(tm, "Thu, 02 Nov 2017 10:00:00 GMT")
    'http://example.org/M1'
    >>> negotiate_on_datetime(tm, "Thu, 02 Nov 2017 09:59:59 GMT")
    'http://example.org/M2'
    >>> negotiate_on_datetime(tm, "Thu, 02 Nov 2017 09:59:59 GMT", method=TimeMap.CLOSEST)
    'http://example.org/M1'
    >>> negotiate_on_datetime(tm, "Mon, 01 Feb 1991 01:01:01 GMT")
    'http://example.org/M2'

Additional Memento Support
--------------------------

Applications supporting Memento datetime negotiation may also need to return descriptions of the set of versions available in the ``application/link-format`` format. This is supported by the ``TimeMap.serialize_link_format`` method:

    >>> from negotiator2 import TimeMap
    >>> tm = TimeMap()
    >>> tm.set_original("http://example.org/R", "Thu, 02 Nov 2017 16:29:00 GMT")
    >>> tm.add_memento("http://example.org/M1", "Thu, 02 Nov 2017 10:00:00 GMT")
    >>> tm.add_memento("http://example.org/M1", "Wed, 01 Nov 2017 09:00:00 GMT")
    >>> tm.timegate = "http://example.org/TG"
    >>> tm.timemap = "http://example.org/TM"

    >>> print(tm.serialize_link_format())
    <http://example.org/R>
      ;rel="original",
    <http://example.org/M1>
      ;rel="memento"
      ;datetime="Thu, 02 Nov 2017 10:00:00 GMT",
    <http://example.org/M1>
      ;rel="memento"
      ;datetime="Wed, 01 Nov 2017 09:00:00 GMT",
    <http://example.org/TG>
      ;rel="timegate",
    <http://example.org/TM>
      ;rel="self"
