Consuming a REST API in #python is as simple as making a #request. The #requests Python package takes the headache out of manually writing API requests. It allows you to parameterize your query, and apply the correct headers. Finally, in classic Python fashion serves the results in #json serializable dictionaries. Following these simple steps, you can be on your way to consuming APIs yourself!
Before we begin, make sure you have the requests package installed in your Python environment. (I’m on Windows, but if you’re on Linux/Mac, use pip3)
Setup
pip install requests
Now, in a new Python script we can prepare our code that will help in calling the REST API. For this example, we will use an API free to use provided by https://www.openbrewerydb.org/ (Yum! But I’m not sponsored! Also drink responsibly!). Looking through their documentation you will notice that in order to get a list of breweries. The URL to target is:
https://api.openbrewerydb.org/breweries
Now, this is a common pattern that REST APIs follow. This URL can be generally broken into two pieces. The base URL and the endpoint.
Before we go ahead and construct the request in Python, we must consider more requirements that go into building an REST API request:
- The request headers, typically and most commonly, this can just consist of content_type: application/json. This denotes that the request is expect JSON data to be returned. It’s usually best to consult the documentation of the API you are consuming. This ensures you assign the correct headers for your request.
- The request method. Since we are fetching or getting information, this will be a GET request. There are other requests types like PUT, POST, DELETE, PATCH. That’s not all, you can check out all HTTP methods here.
- Finally, and optionally (usually), are parameters. Parameters allow you to fine tune your request and apply filters, so you get exactly what you want. We will touch on parameters a little later.
Our First Request
Now we can begin constructing our API request in Python!
>>> url = "https://api.openbrewerydb.org/breweries"
>>> headers = {'content_type': 'application/json'} # We are receiving JSON data.
>>> request_method = 'GET' # We are only fetching information.
>>> result = requests.request(request_method, url, headers=headers)
Just like 1, 2, 3 (and 4) we have our API results! The result variable is actually a Response which contains informative attributes that will help!
Firstly, responses always come with a status code. This lets the consumer know what was the outcome of the request that the server processed. We always hope for a status code of 200. This means the server successfully processed the request and we likely have data to work with. It will also return other codes, like 500, 404, 403, and others. You can look into what those codes mean here. You can view the status code for your response object as follows:
>>> result.status_code
200
Given that we have a successful response, we can start digging into the data! Let’s see what the first brewery that the server returned to us was. We’ll use the json package to format the data for easy reading:
>>> import json
>>> print(json.dumps(result.json()[0], indent=2))
{
"id": "10-56-brewing-company-knox",
"name": "10-56 Brewing Company",
"brewery_type": "micro",
"street": "400 Brown Cir",
"address_2": null,
"address_3": null,
"city": "Knox",
"state": "Indiana",
"county_province": null,
"postal_code": "46534",
"country": "United States",
"longitude": "-86.627954",
"latitude": "41.289715",
"phone": "6308165790",
"website_url": null,
"updated_at": "2023-01-04T04:46:02.393Z",
"created_at": "2023-01-04T04:46:02.393Z"
}
Filter All The Things!
Now, we can stop here; but why should we? Earlier, we touched on parameters. So, to wrap things up – let’s go over how to parameterize a query to get exactly what we want.
Nothing about the URL changes. We are still targeting the /breweries endpoint. However, we are going to add some simple filters in the form of parameters to tune our results a bit.
>>> by_city = "Miami"
>>> by_state = "FL" # There is an imposter Miami located in Ohio state. That'll muddy our results!
Now that we have defined our filters, or parameters, we can include them in a new request.
>>> params = {'by_city': by_city, 'by_state': by_state}
>>> result = requests.request('GET', url, headers=headers, params=params)
>>> print(result.status_code)
200
>>> print(json.dumps(result.json()[0], indent=2))
{
"id": "abbey-brewing-co-miami-beach",
"name": "Abbey Brewing Co",
"brewery_type": "contract",
"street": "1115 16th St",
"address_2": null,
"address_3": null,
"city": "Miami Beach",
"state": "Florida",
"county_province": null,
"postal_code": "33139-2441",
"country": "United States",
"longitude": "-80.1402943",
"latitude": "25.7890381",
"phone": "3055388110",
"website_url": null,
"updated_at": "2023-01-04T04:46:02.393Z",
"created_at": "2023-01-04T04:46:02.393Z"
}
Diving Deep
Now that we have covered the basics of constructing an API request using python, and viewing the results, let’s take it a step further. We are going to create a class that abstracts away a lot of this. This facilitates an easier time writing code, and makes it easier to consume and for others to use too. We can create a class that will process all the required facets that go into an API request. If any are missing, we can either extrapolate or insert default values to make sure our code doesn’t fail.
The following code snippet is going to be long. I urge you to read it in its entirety and then we will review at length:
import requests
from enum import Enum
class Methods(Enum):
GET = 'GET'
POST = 'POST'
# You can assign other methods inside the enum here if necessary for required methods you are using.
class ApiWrapper:
"""Wrapper class for a RESTful API."""
def __init__(self, base_url):
self.base_url = base_url
self.headers = {}
def _send_request(self, method, endpoint, headers=None, params=None):
"""
Used to GET (or provided method) from `endpoint` to interact with
the API.
Parameters
----------
method: str
endpoint: str
headers: dict
params: dict
"""
if not hasattr(Methods, method):
raise ValueError('Invalid Param: {}'.format(method))
if not headers:
headers = {'content_type': 'application/json'}
self.headers = headers
endpoint = "/" + endpoint if not endpoint.startswith('/') else endpoint
if params:
res = requests.request(
method, endpoint_url, headers=headers, params=params)
else:
res = requests.request(method, endpoint_url, headers=headers)
if res.status_code != 200:
raise requests.HTTPError(res)
# Raise an error and surface the the response,
this include the status code.
return res.json()
The _send_request protected method takes an HTTP method (that it expects to be a member of the Methods enumerated class). Also the endpoint, along with the headers and parameters – as optional kwargs, or keyword arguments. The ApiWrapper class can be extended with additional methods that help define the endpoint for the user. Furthermore, this method can take parameters as arguments, the wrapper class will automatically pack into a dictionary to send to the REST API for processing.
Thank your for reading! I regularly write articles on here and on LinkedIn so feel free to follow, or connect!
I am always happy to answer questions, so feel free to comment below or reach out via email at mrodriguez@datamunch.io and I will get back to you as soon as I can. Thanks again!
Image Credits:
By Requests – [1], Apache License 2.0, https://commons.wikimedia.org/w/index.php?curid=70961176
By www.python.org – www.python.org, GPL, https://commons.wikimedia.org/w/index.php?curid=34991651