A flexible approach to Python API client development

Are you working in a microservice-oriented architecture? Is there an API you want to use that doesn't have a Python SDK? Although high-level HTTP libraries like requests aid development by reducing the work you need to do to get up and running, they don't provide much structure around APIs specifically. This can lead to several pieces of similar calling code peppered throughout your system, in the worst cases leading to divergent approaches.

Let's take a look at what a set of API calls could look like using requests. I'll use the PokéAPI, a fantastic (and free!) API for querying Pokémon metadata. The most useful endpoint is /pokemon/:pokemon, which is the main entrypoint for getting information about a particular Pokémon. You can call this endpoint using requests:

>>> import requests
>>> ditto = requests.get('https://pokeapi.co/api/v2/pokemon/ditto')

This returns a bevy of information, much of which links to other endpoints. To inspect the data more easily, you'll want to get the returned JSON as a Python dictionary:

>>> ditto = requests.get('https://pokeapi.co/api/v2/pokemon/ditto').json()

You can verify you've received a response about Ditto by checking its name:

>>> ditto['name']

You can also see that Ditto has one move, named "transform":

>>> ditto['moves'][0]['move']['name']

You can also get the endpoint for learning more about the transform move:

>>> ditto['moves'][0]['move']['url']

You can then call the /move/:move_id endpoint using requests to get its info. That will lead to another response which links to yet further endpoints. There are so many endpoints! They're all under the same PokéAPI umbrella, but there's not much structure in the code which reflects this fact. If some of these endpoints return a single string value instead of JSON, the code to interact with the data will need to change as well. Is there a better way to keep this all straight?

At scale, it's important to understand all the APIs and endpoints your code is calling, in part so that you don't spend effort creating code that duplicates existing functionality. It also makes it easier to survey usage to understand if you've migrated fully away from a deprecated API or endpoint. To do this you need a single source of truth for an API and its endpoints, and ideally a homogeneous way of interacting with them.

apiron is a Python package that addresses these desires by providing a declarative approach that produces SDK-like interaction for you to use. Through introspection of your declared configuration, apiron allows you to start talking to an API quickly and leads you toward a centralized configuration for all of your API dependencies. Let's look at how you might set up the PokéAPI using apiron.

With apiron you can define a Service, which has a domain and a collection of Endpoints. The Pokémon and move endpoints happen to return JSON, so using a JsonEndpoint will end up returning the response as a dictionary by default. Each endpoint has placeholders that can be filled in dynamically:

from apiron import Service, JsonEndpoint

class PokeAPI(Service):
    domain = 'https://pokeapi.co'

    pokemon = JsonEndpoint(path='/api/v2/pokemon/{pokemon}')
    move = JsonEndpoint(path='/api/v2/move/{move_id}')

 This is all fine, but how do you interact with it? apiron provides SDK-like interaction for calling your configured service. That is, apiron tries to feel like something purpose-built for the API you need to use while being flexible enough to do this for any number of APIs. Here's how to make the call to get Ditto's information and information about the transform move:

ditto = PokeAPI.pokemon(path_kwargs={'pokemon': 'ditto'})
transform = PokeAPI.move(path_kwargs={'move_id': 144})

From this code, it's easier to see that you're calling the PokéAPI and using the Pokémon and move endpoints. It's also easier to see what data is being plugged into the calls. If you're using an IDE, you can also jump to those endpoint definitions to see that they return JSON, so you get an idea of what to expect when using the responses. As you look through the PokeAPI class, you can also see which endpoints are already implemented at a glance, and quickly add any that aren't available yet if you know the right path and returned data type.

If the PokéAPI moves to a new domain, or if an endpoint's path changes, there's one clear place to react to that change. You can also call an endpoint with different headers, cookies, and more without having to duplicate much boilerplate. The readability and inspectability of this approach have improved my experience dealing with numerous services with numerous endpoints. In addition to helping users develop clients for APIs they want to use, this package can potentially reduce the effort needed for API providers to give their consumers a Python SDK. I really hope you'll try it out and tell me all the ways you can think of to break it!