DEV Community

Dane Hillard
Dane Hillard

Posted on • Originally published at dane.engineering

A flexible approach to Python API client development

Ditto Pokémon

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')
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

>>> ditto['name']
'ditto'
Enter fullscreen mode Exit fullscreen mode

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

>>> ditto['moves'][0]['move']['name']
'transform'
Enter fullscreen mode Exit fullscreen mode

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

>>> ditto['moves'][0]['move']['url']
'https://pokeapi.co/api/v2/move/144/'
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo ithaka / apiron

🍳 apiron is a Python package that helps you cook a tasty client for RESTful APIs. Just don't wash it with SOAP.

apiron

PyPI version Supported Python versions Build status Documentation Status Contributor Covenant

apiron helps you cook a tasty client for RESTful APIs. Just don't wash it with SOAP.

Pie in a cast iron skillet

Gathering data from multiple services has become a ubiquitous task for web application developers The complexity can grow quickly calling an API endpoint with multiple parameter sets calling multiple API endpoints, calling multiple endpoints in multiple APIs. While the business logic can get hairy, the code to interact with those APIs doesn't have to.

apiron provides declarative, structured configuration of services and endpoints with a unified interface for interacting with them.

Defining a service

A service definition requires a domain and one or more endpoints with which to interact:

from apiron import JsonEndpoint, Service

class GitHub(Service):
    domain = 'https://api.github.com'
    user = JsonEndpoint(path='/users/{username}')
    repo = JsonEndpoint(path='/repos/{org}/{repo}')
Enter fullscreen mode Exit fullscreen mode

Interacting with a service

Once your service definition is in place, you can interact…

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}')
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
rhymes profile image
rhymes

Very interesting! I'm slowly writing a REST API client using requests directly, I might take a look at this library as well :-)

Collapse
 
easyaspython profile image
Dane Hillard

Awesome, I hope you do, even if you don't end up liking it! Feedback is welcome 😄