Requests’ Two APIs

Kenneth Reitz’s excellent
Requests library has been
praised, rightfully, for its excellent API. In fact, its API is so good that
it’s been praised in a
literary context, as well as by
almost every programmer who has come across it. There is no question that this
API is one of the best you can find in the Python world. It has even inspired
other programmers’ API design.

What a lot of people seem not to know, however, is that Requests actually has
two APIs. The first API is the highly-praised procedural and object-oriented
API that drives making and interacting with HTTP traffic. The second API
appeared when Kenneth undertook the
giant refactor for v1.0.
This is Requests’ barely heralded inheritance-based API.

Requests’ Inheritance API

To discuss the second, super secret Requests API we need to draw one very
import distinction. This distinction is between properties of a single HTTP
request (things like data, headers and cookies) and things that are properties
of the connection (things like the kinds of SSL you can use, or what IP address
you send data from).

Requests by and large exposes the first kind of property on its ‘standard’ API.
You can set the headers, and the body data, and the cookies, all through the
arguments to the Request object and as properties of the Session object. This
makes sense: this is the kind of thing that is really specific to a single
request or session of requests.

But what about the second kind of thing? Where do you set that? Some of the
most-used stuff bubbles its way up to the main API so that it’s easily
accessible, but most of it is hardcoded.

I can hear some of you now. “Kenneth hardcoded these options? How awful!
In the Requests versions v0.14.2 and earlier, I’d have probably agreed with
you. They aren’t options people want often, but it would be good to have access
to them.

The problem is: how do you give access to these options without cluttering the
main API so much your documentation looks
like this?

Configuration Through Inheritance

If you look carefully at the Requests code, you’ll notice that there are two
major objects involved in each request. The first is the Requests Session
object (requests.Session). The second is the Requests default
Transport Adapter,
the HTTPAdapter (requests.adapters.HTTPAdapter). These two objects
maintain all of the configuration in Requests.

The Session object is almost entirely concerned with the first kind of
property. You can set default headers and default cookies, and that all goes
really well. But what if you want to handle the second kind? The answer is
surprisingly simple: subclass the HTTPAdapter.

The HTTPAdapter manages all of the connection-level behaviour, and doesn’t
expose much in the way of options on its constructor. The real way to affect
this behaviour is to subclass it and create your own.

Examples

I’ve shown this configuration
once before on this blog,
but I want to provide a few examples of what I mean. The first is one I’ve
shown before: selecting an SSL version.

from requests.adapters import HTTPAdapter
from requests.packages.urllib3.poolmanager import PoolManager

class SSLAdapter(HTTPAdapter):
    '''An HTTPS transport adapter that uses an arbitrary SSL version.'''
    def __init__(self, ssl_version=None, **kwargs):
        self.ssl_version = ssl_version
        super(SSLAdapter, self).__init__(**kwargs)

    def init_poolmanaager(self, connections, maxsize):
        self.poolmanager = PoolManager(num_pools=connections,
                                       maxsize=maxsize,
                                       ssl_version=self.ssl_version)

You can unconditionally sign requests:

from requests.adapters import HTTPAdapter
from requests.packages.urllib3.poolmanager

class SigningAdapter(HTTPAdapter):
    """This adapter signs a request using some unknown method."""
    def send(self, request, **kwargs):
        signature = signing_function(request.body)
        request.headers['Signature'] = signature

        super(SigningAdapter, self).send(request, **kwargs)

You can even support
totally different protocols
(though having done that myself, I don’t entirely recommend it, but it can
be done).

Why This Works

The reason this works is because Requests allows you to configure the backend,
using Session.mount(). Before you could do this, anything that was not
contained in the Session object was totally inaccessible to users. This meant
if you wanted to change the sending logic, you had to subclass the Session and
get your hands dirty in the 2000-line send function.

Now, you get to subclass the clearly laid out HTTPAdapter and have to
rewrite as little as possible. More people need to know about this and use it,
because it’s the best way to really take control of Requests.

Requests has never been more modular or powerful, and it’s API is just as clean
as it was when it was born. Take advantage of this secondary API, and you’ll
find that you can configure Requests just as easily as you can drive it.