Categories
API python

Building a rest API client or SDK as a python package

The best way to learn ways of building your API wrapper / SDK is to build it yourself from scratch is to look at ones that already exist. Here is a list of python api wrappers you can checkout and view. Most if not all of them are already python packages.

Here is a good post on how to test and build an API client / wrapper. In the post the author uses vcrpy as a way to replay stored responses, however they do actually happen the first time. To prevent your tests from ever hitting a real API you should look at Responses.

I also found a few generic wrappers for the requests library that are supposed to make your life easier, but it looked like many were just abstraction for abstraction sake and the closer your are to requests the better.

I did however find uplink which is a python api boilerplate module based on retrofit – which is the goto http client for Android development. I like it and I think it will make my life a bit easier when creating the api wrapper. It is however still in beta. Tapioca is another one, but I didn’t rate it. It arised from this fellow not liking how API wrappers are done and the added abstraction.

As a side note, Braintree doesn’t even make the rest API available, it forces clients to use a client or wrapper built by them mainly to ensure security.

I found uplink hard to work with…it seems as though the further you are from requests the harder your life becomes.

Allowing for a User Provided Session

One of the key things needed when making an API wrapper/client available is letting the user provide their own session. A reason for this is that user could have added additional auth to the API they are connecting to or may be using a different method of auth altogether.

Also, the client may be using an API gateway like kong so the client may need an auth token or some other permission. So it is better to just allow the user to provide their own session – which can then be modified with the session id’s.

So in the class constructor – __init__() method, ensure a session can be set and default it to none. The below example is adapted from the hvac client. eg:


class MyClient(object):
    """My Api Client"""

    def __init__(self, base_uri=DEFAULT_BASE_URI, cert=None, verify=True, timeout=30, proxies=None, session=None):
        """Create a new request adapter instance.

        :param base_uri: Base URL for the Vault instance being addressed.
        :type base_uri: str
        :param cert: Certificates for use in requests sent to the Vault instance. This should be a tuple with the
            certificate and then key.
        :type cert: tuple
        :param verify: Either a boolean to indicate whether TLS verification should be performed when sending requests to Vault,
            or a string pointing at the CA bundle to use for verification. See http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification.
        :type verify: Union[bool,str]
        :param timeout: The timeout value for requests sent to Vault.
        :type timeout: int
        :param proxies: Proxies to use when preforming requests.
            See: http://docs.python-requests.org/en/master/user/advanced/#proxies
        :type proxies: dict
        :param session: Optional session object to use when performing request.
        :type session: request.Session
        """
        if not session:
            session = requests.Session()

        self.base_uri = base_uri
        self.token = token
        self.namespace = namespace
        self.session = session
        self.allow_redirects = allow_redirects

        self._kwargs = {
            'cert': cert,
            'verify': verify,
            'timeout': timeout,
            'proxies': proxies,
        }

As you can see in the example above it is not just the session. Other things like SSL verification, the certificate to use for SSL verification and proxies can be passed to the constrator to allow for versatility.

Should the client raise it’s own Errors

If a request on your managed node API returns an error 4xx or 5xx, should your API client raise an exception or just let the response be handled by the caller?

Say you do catch exceptions, should those exceptions be custom created by the client package or just be part of pythons standard lib, such as ValueError.

From what I have seen, many python api clients do raise their own errors stemming from a root exception. According to Brett Slatkin of Effective Python:

Having a root exception lets consumers of your API catch exceptions you raise on purpose

They should help you find bugs, not hide them.

Some examples:

Although you can choose to use the standard libraries excpetions if you want, an example of this would be the DEPRECATED heroku python client wrapper or the new heroku python client wrapper

How to store a session based token that gets Revoked

Different API’s authenticate in different ways. Some will provide you a token that only lasts a certain length of time, thereafter your requests stop being authenticated.

How should we handle this? Get a new token each time – probably not a good idea, store the token in a key value store or store it as a global variable.

Also what mechanism should we use to ensure a new token is requested, when the old one expires?

How to automatically reauth for a specific error code or catch errors

In order to catch these error codes and raise our own, it is best to do it in a single place. Pretty much however the client is making requests – perhaps with the requests library request method. We would have to extend from the request class and do our clients error checking after we get a response – in our custom requests class.

One way of doing that is how hvac used in the adapter class.

The problem is that we are allowing users of the library to provide their own requests.Session() object, that won’t be using our custom request method.

The idea here is to have your client class define it’s own request method and call that method instead of using requests. In other words MyApiClient.get() which behind the scenes uses self.session.get, but adds error checking.