![WebAPI_and_you.jpg](https://cdn-learn.adafruit.com/user_assets/assets/000/000/891/large1024/WebAPI_and_you.jpg?1716070972)
- An uncomprehensive & comprehensible guide to using Adafruit Requests with web API's.
The amount of online API examples for the Adafruit_Requests library is growing. If you're interested in using an API but an example does not exist and you're unsure how to start I will help walk you through the process.
I wrote the majority of the web API examples currently in the examples directory. I'm a pretty good source on how to approach a new unknown web API and wrangle out some basic data.
JSON
Data is malleable, it can be shaped and formed in a variety of ways. The most common format currently used with REST API's is JSON. The Adafruit_Requests library is particularly good with JSON data. If you don't know how to read or write in JSON don't worry, you won't need to. The library handles all the format conversions for you. All you need to know is how to get at the data you want and I will walk you through how to do that.
Before we begin there are some glossary terms that you'll need to know in order to make sense of JSON for the web.
REST
Representational State Transfer (REST) is a software architecture that imposes conditions on how an API should work. Most web API's use the REST architecture. All a beginner needs to know about REST is it's the way a website allows you to retrieve data from an API.
Endpoint
An endpoint refers to the data to retrieve from an API. This can be part of the JSON heirarchy path or a key:value pair. For web API's typically this refers to the JSON Key:Value pair.
Key:Value
- Key:Value (always a pair separated by a colon)
A key is a unique identifier associated with data and the value is the actual data. An example would be if you're working with a weather API and might want to return current:temperature for a geographic area to display freedom units on a TFT. It would return as `current:70.0` (in Fahrenheit) or if you're living in a sensible country using the metric system it would return `current:21.1` (in Celsius).
Key Error
It's useful to know the above terms because an error that a website or Circuit Python might return is `invalid key:value pair` or simply `key error`. That error means you requested a key:value that does not exist, you spelled it incorrectly, or the request was otherwise incorrectly formatted.
Required Libraries
All libraries used here are either built into Circuit Python or can be downloaded from the latest stable release bundle from CircuitPython.org/libraries. Place external libraries downloaded from the bundle into your CIRCUITPY device's /lib directory. Only ever place the external libraries you are actively using into your /lib directory.
Built-in Libraries
- os
- time
- wifi
External Libraries (from the bundle)
- adafruit_requests
- adafruit_connection_manager
RocketLaunch.Live API example
For the walk through I will be using the RocketLaunch.Live API example because it's a relatively easy to use and a publicly accessible API. A public API means you don't need any special credentials to pass with the request. A private example means you must pass some type of credentials such as an oauth bearer token or login with the request.
Library Imports
First we'll need to import the libraries at the top of your code.py file to be used with this example. As of Circuit Python 9.0 imports are grouped according to Python PEP-8. That's just a way of saying imports are now grouped by:
- standard library
- 3rd party library
- local library
If you're an absolute beginner you don't need to worry about import order groupings unless you intend to submit your API example to the adafruit_requests repository. You can import them in any order you want if you're just beginning.
import os import time import adafruit_connection_manager import wifi import adafruit_requests
Sleep Timer (API Polling Rate)
Next we setup a customizable poll timer. SLEEP_TIME is widely used in our API examples for the delay between API fetches. It's not an official variable just a name that is commonly used. You can configure the poll time in seconds to whatever you like.
Some API's will allow you to make a request at maximum of once per minute. Other API's it might be once per 5 minutes or 15 minutes.
Just be aware that most API's have request limits and if you exceed too many requests in a certain time period you could be temporarily banned. A temporary ban can range from 1 hour to 24 hours and is a way a website protects itself from being flooded with too many requests.
# Time between API refreshes # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour SLEEP_TIME = 43200
WiFi Credential Setup
Now we will configure the WiFi settings. Your WiFi credentials should exist in your settings.toml file on your CIRCUITPY board (device).
# Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
Time Calc
The time_calc function is a custom function I insert into most of my examples. It converts seconds into human readable minutes, hours, or days. This is typically used to return the time until the next update so it will say something like next update: 12 hours instead of next update: 43200. Converting to human readable time is much easier to understand; otherwise you will be converting seconds in your head or using a calculator. The time calc function does it for you.
You can test the function anywhere in the script by calling time_calc(500) for example and it will convert 500 seconds into human readable format.
def time_calc(input_time): """Converts seconds to minutes/hours/days""" if input_time < 60: return f"{input_time:.0f} seconds" if input_time < 3600: return f"{input_time / 60:.0f} minutes" if input_time < 86400: return f"{input_time / 60 / 60:.0f} hours" return f"{input_time / 60 / 60 / 24:.1f} days"
JSON GET Source
Now we define our GET source which should be a valid JSON formatted page. If you visit the URL in your web browser:
https://fdo.rocketlaunch.live/json/launches/next/1
you should see a JSON formatted hierarchy structure. This is the JSON url that the adafruit_requests library will perform a GET request on. You can name the source variable whatever you like as long as it is specified with the same variable name. Using the GET source is covered later in the script.
# Publicly available data no header required # The number at the end is the amount of launches (max 5 free api) ROCKETLAUNCH_SOURCE = "https://fdo.rocketlaunch.live/json/launches/next/1"
SocketPool & Connection_Manager
Now we initialize the most important variables of the entire script which is the socket pool, ssl context, and session. This is what will initiate a socket connection between your device and the URL using Adafruit_Requests (this can also be a localhost url or IP address). It also initializes the wifi radio and creates an active session via the Adafruit Connection Manager library. The Connection Manager is what automatically manages all of your socket connections.
# Initalize Wifi, Socket Pool, Request Session pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) requests = adafruit_requests.Session(pool, ssl_context)
while True: (infinite loop)
If you only want a script to run once then do not include while True: in your code.
Everything inside the while True loop will loop infinitely. With an infinite online loop the first thing is to ensure the wifi radio is capable of connecting to the host URL. If wifi is ever disconnected it will automatically reconnect prior to attempting to contact the host again for the next poll update.
while True: # Connect to Wi-Fi print("\n===============================") print("Connecting to WiFi...") while not wifi.radio.ipv4_address: try: wifi.radio.connect(ssid, password) except ConnectionError as e: print("❌ Connection Error:", e) print("Retrying in 10 seconds") print("✅ Wifi!")
Error Handling
All code to be run on the API should be enclosed within a try/except error handler. In the event an error occurs it can process the error in a way you specify and potentially recover so the script doesn't crash. Without a try/except error handler for a 24/7 online script the entire script will eventually crash.
Connection errors to the host could fail for a variety of reasons including; host timeout, unauthorized, wrong key:value pair, and the list goes on. By using an error handler you can catch and ignore the most common errors allowing the script to continue. At worst you will miss a poll update but your script will continue to function instead of crashing. It's very important to learn how to use error handlers with Circuit Python for a 24/7 online infinite looping script.
try: # Print Request to Serial print(" | Attempting to GET RocketLaunch.Live JSON!") time.sleep(2) debug_rocketlaunch_full_response = False try: with requests.get(url=ROCKETLAUNCH_SOURCE) as rocketlaunch_response: rocketlaunch_json = rocketlaunch_response.json() except ConnectionError as e: print("Connection Error:", e) print("Retrying in 10 seconds") print(" | ✅ RocketLaunch.Live JSON!") if debug_rocketlaunch_full_response: print("Full API GET URL: ", ROCKETLAUNCH_SOURCE) print(rocketlaunch_json) # JSON Endpoints RLFN = str(rocketlaunch_json["result"][0]["name"]) RLWO = str(rocketlaunch_json["result"][0]["win_open"]) TZERO = str(rocketlaunch_json["result"][0]["t0"]) RLWC = str(rocketlaunch_json["result"][0]["win_close"]) RLP = str(rocketlaunch_json["result"][0]["provider"]["name"]) RLVN = str(rocketlaunch_json["result"][0]["vehicle"]["name"]) RLPN = str(rocketlaunch_json["result"][0]["pad"]["name"]) RLLS = str(rocketlaunch_json["result"][0]["pad"]["location"]["name"]) RLLD = str(rocketlaunch_json["result"][0]["launch_description"]) RLM = str(rocketlaunch_json["result"][0]["mission_description"]) RLDATE = str(rocketlaunch_json["result"][0]["date_str"]) # Print to serial & display label if endpoint not "None" if RLDATE != "None": print(f" | | Date: {RLDATE}") if RLFN != "None": print(f" | | Flight: {RLFN}") if RLP != "None": print(f" | | Provider: {RLP}") if RLVN != "None": print(f" | | Vehicle: {RLVN}") # Launch time can sometimes be Window Open to Close, T-Zero, or weird combination. # Should obviously be standardized but they're not input that way. # Have to account for every combination of 3 conditions. # T-Zero Launch Time Conditionals if RLWO == "None" and TZERO != "None" and RLWC != "None": print(f" | | Window: {TZERO} | {RLWC}") elif RLWO != "None" and TZERO != "None" and RLWC == "None": print(f" | | Window: {RLWO} | {TZERO}") elif RLWO != "None" and TZERO == "None" and RLWC != "None": print(f" | | Window: {RLWO} | {RLWC}") elif RLWO != "None" and TZERO != "None" and RLWC != "None": print(f" | | Window: {RLWO} | {TZERO} | {RLWC}") elif RLWO == "None" and TZERO != "None" and RLWC == "None": print(f" | | Window: {TZERO}") elif RLWO != "None" and TZERO == "None" and RLWC == "None": print(f" | | Window: {RLWO}") elif RLWO == "None" and TZERO == "None" and RLWC != "None": print(f" | | Window: {RLWC}") if RLLS != "None": print(f" | | Site: {RLLS}") if RLPN != "None": print(f" | | Pad: {RLPN}") if RLLD != "None": print(f" | | Description: {RLLD}") if RLM != "None": print(f" | | Mission: {RLM}") print("\nFinished!") print(f"Board Uptime: {time_calc(time.monotonic())}") print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") except (ValueError, RuntimeError) as e: print("Failed to get data, retrying\n", e) time.sleep(60) break time.sleep(SLEEP_TIME)
Serial Debug Setup
More experienced coders won't need a line by line replay of what every line means. As a beginner you might not even be interested either but if you want to learn how everything works it's good to know what every line does especially how to pull endpoints.
We'll start with serial debugging (which simply means adding print statements to see what the code is doing at a particular point in the code). If you are using Mu you can click on the serial button to display the REPL and any serial debugging will be displayed there.
The first print statement notifies you in your serial terminal/console/REPL that an attempt to connect to the API is about to occur. This is useful so that if the script fails prior to this point you'll know that a connection attempt wasn't even made. The variable for debugging can be named anything. I intentionally name most variables in every script something different so you can copy and paste a variety of examples together with minimal manual code changes.
A short time.sleep(2) is a 2 second time delay. It isn't completely necessary but in the event something goes wrong it can help prevent spamming connection attempts every x milliseconds which might get your temp banned by the API (all API's have different rules about connection timings).
# Print Request to Serial print(" | Attempting to GET RocketLaunch.Live JSON!") time.sleep(2) debug_rocketlaunch_full_response = False
GET Request
This is where the actual get request for the source (variable you specified earlier) takes place. It should be within a try/except block (error handler). This is the most prone to any wifi connections or routing disconnections that will throw an error. It will TRY to connect and if it cannot it will throw an exception error. You can name the error variable anything you want. e is commonly used as short-hand for error. e is a variable that holds the error code to be printed.
It will continue to try to connect infinitely until it succeeds. Once it does succeed it will print out the message "RocketLaunch.Live JSON!" This lets you know that it has at the very least connected to the API and attempted to retrieve data. This doesn't mean it's successful in pulling data only that the connection to the host JSON was successful.
Even though adafruit does have a separate importable JSON library it's built into adafruit_requests so you don't need a separate json import at the beginning of the script for this type of connection method. Using .json() will automatically parse the JSON data for you.
By using a with statement it will automatically close a socket when you're done with it. This is a recent development in Circuit Python 9 so you don't have to manually close a socket anymore, it's all handled automatically. Socket management is much more robust in Circuit Python 9 than it was in Circuit Python 6, 7, or 8.
try: with requests.get(url=ROCKETLAUNCH_SOURCE) as rocketlaunch_response: rocketlaunch_json = rocketlaunch_response.json() except ConnectionError as e: print("Connection Error:", e) print("Retrying in 10 seconds") print(" | ✅ RocketLaunch.Live JSON!")
Serial Debugging
This is where we can print out debugging if you set debug_rocketlaunch_full_response to = True
This type of debugging is the shotgun approach when you can't figure out why a value isn't returning correctly. It will print everything from the requested URL not just the data you want but EVERYTHING. As you can see from the JSON browser screenshot above that can be a lot of data. If it's for a very data heavy API like youtube, github, or steam it will return so much data that it can crash your microcontroller.
Only enable debugging if you need a better idea of the data structure and honestly don't mind if your microcontroller crashes. Fair warning.
if debug_rocketlaunch_full_response: print("Full API GET URL: ", ROCKETLAUNCH_SOURCE) print(rocketlaunch_json)
Return Parsed Endpoint Data
rocketlaunch_json represents the top level directory of the returned JSON hierarchy structure. List item keys are setup like a path to the value you want to pull. Let's use the rocket launch vehicle name as an example for data to retrieve.
Here is an easy way to correlate the JSON parsed path to the JSON web browser data structure. We want to put it into a variable name for use later on.
RLFN = str(rocketlaunch_json["result"][0]["name"])
In the above example result is a key, 0 is a key, and name is a key. We will retrieve the value for the key name. If you receive an error for "invalid key" or "invalid key:value" now you can see how that error correlates to a key or key:value pair.
So what happens if you just retrieve ['result'] or ['result][0]? It will return everything that is listed under the requested key as a parsed list. This can be useful for navigating through JSON to drill down to the data you do want.
Now simply repeat this process with the correct paths to any values you want to return. You can use either single or double quotes for keys or values, both work fine with adafruit_requests json parsing.
# JSON Endpoints RLFN = str(rocketlaunch_json["result"][0]["name"]) RLWO = str(rocketlaunch_json["result"][0]["win_open"]) TZERO = str(rocketlaunch_json["result"][0]["t0"]) RLWC = str(rocketlaunch_json["result"][0]["win_close"]) RLP = str(rocketlaunch_json["result"][0]["provider"]["name"]) RLVN = str(rocketlaunch_json["result"][0]["vehicle"]["name"]) RLPN = str(rocketlaunch_json["result"][0]["pad"]["name"]) RLLS = str(rocketlaunch_json["result"][0]["pad"]["location"]["name"]) RLLD = str(rocketlaunch_json["result"][0]["launch_description"]) RLM = str(rocketlaunch_json["result"][0]["mission_description"]) RLDATE = str(rocketlaunch_json["result"][0]["date_str"])
[0] is not the same as ['0']. [0] is a list and ['0'] is a value. Getting that wrong will lead to an exception error for invalid key.
Printing Values to Serial Console (REPL)
All API's are slightly different. This API will actually return a literal value of 'None' if an endpoint is blank. Not all API's do that. This is quite convenient as any valid endpoint will never produce an error for not existing and makes it an excellent API to start with as a beginner.
We can setup some checks using if/else statements to see if a value is 'None' and if it is then don't return it. You can use the if statements to print something else such as "Currently No Value" but in this case I've decided simply not to return it at all if it's blank.
if RLDATE != "None":
Translates to: if RLDATE does not equal "None". RLDATE endpoint variable was defined earlier above and is the rocket launch date.
# Print to serial & display label if endpoint not "None" if RLDATE != "None": print(f" | | Date: {RLDATE}") if RLFN != "None": print(f" | | Flight: {RLFN}") if RLP != "None": print(f" | | Provider: {RLP}") if RLVN != "None": print(f" | | Vehicle: {RLVN}")
Launch Window Conditionals
There are a combination of possible values for the launch window open, launch window close, and t-minus zero expected launch time. These types of conditions can be accounted for with if/else statements. Using the variable names we set above for all the different endpoints we'll now use those variables to print out a list of expected launch times for a rocket launch.
# Launch time can sometimes be Window Open to Close, T-Zero, or weird combination. # Should obviously be standardized but they're not input that way. # Have to account for every combination of 3 conditions. # T-Zero Launch Time Conditionals if RLWO == "None" and TZERO != "None" and RLWC != "None": print(f" | | Window: {TZERO} | {RLWC}") elif RLWO != "None" and TZERO != "None" and RLWC == "None": print(f" | | Window: {RLWO} | {TZERO}") elif RLWO != "None" and TZERO == "None" and RLWC != "None": print(f" | | Window: {RLWO} | {RLWC}") elif RLWO != "None" and TZERO != "None" and RLWC != "None": print(f" | | Window: {RLWO} | {TZERO} | {RLWC}") elif RLWO == "None" and TZERO != "None" and RLWC == "None": print(f" | | Window: {TZERO}") elif RLWO != "None" and TZERO == "None" and RLWC == "None": print(f" | | Window: {RLWO}") elif RLWO == "None" and TZERO == "None" and RLWC != "None": print(f" | | Window: {RLWC}") if RLLS != "None": print(f" | | Site: {RLLS}") if RLPN != "None": print(f" | | Pad: {RLPN}") if RLLD != "None": print(f" | | Description: {RLLD}") if RLM != "None": print(f" | | Mission: {RLM}")
As you can see in the screenshot the values are 'null' and not 'None' as they once were. API's can and do change frequently as their web developers work on them. An API endpoint might suddenly stop working one day and you'll have to go digging around in the JSON to figure out why. This is a good example of a sudden API change. Now I have to go rewrite the example to account for values of 'null' and/or 'None'.
Disconnection & Uptime
As the script finishes it prints to serial that it is finished processing all requests, shows how long the board has been running since last hard reset (Uptime), and the amount of time until it polls the server again.
time.monotonic() is a built-in time function to Circuit Python that counts upwards from 0 seconds when the board is first powered on. It's a nice way to keep track of time, loosely, as it will lose precision after about 3 days. As long as we only need a rough estimate of time and not precision to the millisecond or second then time.monotonic() serves its purpose well for keeping track of approximate Uptime.
print("\nFinished!") print(f"Board Uptime: {time_calc(time.monotonic())}") print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================")
Final Exception
The last exception from the very first try/except block at the top of the while True loop ends here. It's a general exception catcher. There are a few choices you can make with exceptions.
- continue
- break
- pass
Continue will attempt to retry at the point of the try statement it spawned from. Break will break out of the try loop and go to the top of the while True loop. This might happen if WiFi goes down in the middle of the script for example. By using break it will put it back into an infinite reconnect loop which was the very first thing we specified in the while True loop. pass is basically just an ignore and does nothing.
time.sleep(SLEEP_TIME) will delay the script from running again until the amount of seconds specified in the variable have passed. In this example that means it will delay running again for 12 hours.
except (ValueError, RuntimeError) as e: print("Failed to get data, retrying\n", e) time.sleep(60) break time.sleep(SLEEP_TIME)
Below is a test of the Playground Note code embed directly from Github (must be .md or .py file). If you see the full code below it is working as intended. If the example in the library gets updated this Playground Note might not be kept up to date with future changes. The code below will always be the most up to date as it is pulled directly from the adafruit_requests examples library.
Full Example Code
# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT # Coded for Circuit Python 9.0 """RocketLaunch.Live API Example""" import os import time import adafruit_connection_manager import wifi import adafruit_requests # Time between API refreshes # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour SLEEP_TIME = 43200 # Get WiFi details, ensure these are setup in settings.toml ssid = os.getenv("CIRCUITPY_WIFI_SSID") password = os.getenv("CIRCUITPY_WIFI_PASSWORD") def time_calc(input_time): """Converts seconds to minutes/hours/days""" if input_time < 60: return f"{input_time:.0f} seconds" if input_time < 3600: return f"{input_time / 60:.0f} minutes" if input_time < 86400: return f"{input_time / 60 / 60:.0f} hours" return f"{input_time / 60 / 60 / 24:.1f} days" # Publicly available data no header required # The number at the end is the amount of launches (max 5 free api) ROCKETLAUNCH_SOURCE = "https://fdo.rocketlaunch.live/json/launches/next/1" # Initalize Wifi, Socket Pool, Request Session pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) requests = adafruit_requests.Session(pool, ssl_context) while True: # Connect to Wi-Fi print("\n===============================") print("Connecting to WiFi...") while not wifi.radio.ipv4_address: try: wifi.radio.connect(ssid, password) except ConnectionError as e: print("❌ Connection Error:", e) print("Retrying in 10 seconds") print("✅ Wifi!") try: # Print Request to Serial print(" | Attempting to GET RocketLaunch.Live JSON!") time.sleep(2) debug_rocketlaunch_full_response = False try: with requests.get(url=ROCKETLAUNCH_SOURCE) as rocketlaunch_response: rocketlaunch_json = rocketlaunch_response.json() except ConnectionError as e: print("Connection Error:", e) print("Retrying in 10 seconds") print(" | ✅ RocketLaunch.Live JSON!") if debug_rocketlaunch_full_response: print("Full API GET URL: ", ROCKETLAUNCH_SOURCE) print(rocketlaunch_json) # JSON Endpoints RLFN = str(rocketlaunch_json["result"][0]["name"]) RLWO = str(rocketlaunch_json["result"][0]["win_open"]) TZERO = str(rocketlaunch_json["result"][0]["t0"]) RLWC = str(rocketlaunch_json["result"][0]["win_close"]) RLP = str(rocketlaunch_json["result"][0]["provider"]["name"]) RLVN = str(rocketlaunch_json["result"][0]["vehicle"]["name"]) RLPN = str(rocketlaunch_json["result"][0]["pad"]["name"]) RLLS = str(rocketlaunch_json["result"][0]["pad"]["location"]["name"]) RLLD = str(rocketlaunch_json["result"][0]["launch_description"]) RLM = str(rocketlaunch_json["result"][0]["mission_description"]) RLDATE = str(rocketlaunch_json["result"][0]["date_str"]) # Print to serial & display label if endpoint not "None" if RLDATE != "None": print(f" | | Date: {RLDATE}") if RLFN != "None": print(f" | | Flight: {RLFN}") if RLP != "None": print(f" | | Provider: {RLP}") if RLVN != "None": print(f" | | Vehicle: {RLVN}") # Launch time can sometimes be Window Open to Close, T-Zero, or weird combination. # Should obviously be standardized but they're not input that way. # Have to account for every combination of 3 conditions. # T-Zero Launch Time Conditionals if RLWO == "None" and TZERO != "None" and RLWC != "None": print(f" | | Window: {TZERO} | {RLWC}") elif RLWO != "None" and TZERO != "None" and RLWC == "None": print(f" | | Window: {RLWO} | {TZERO}") elif RLWO != "None" and TZERO == "None" and RLWC != "None": print(f" | | Window: {RLWO} | {RLWC}") elif RLWO != "None" and TZERO != "None" and RLWC != "None": print(f" | | Window: {RLWO} | {TZERO} | {RLWC}") elif RLWO == "None" and TZERO != "None" and RLWC == "None": print(f" | | Window: {TZERO}") elif RLWO != "None" and TZERO == "None" and RLWC == "None": print(f" | | Window: {RLWO}") elif RLWO == "None" and TZERO == "None" and RLWC != "None": print(f" | | Window: {RLWC}") if RLLS != "None": print(f" | | Site: {RLLS}") if RLPN != "None": print(f" | | Pad: {RLPN}") if RLLD != "None": print(f" | | Description: {RLLD}") if RLM != "None": print(f" | | Mission: {RLM}") print("\nFinished!") print(f"Board Uptime: {time_calc(time.monotonic())}") print(f"Next Update: {time_calc(SLEEP_TIME)}") print("===============================") except (ValueError, RuntimeError) as e: print("Failed to get data, retrying\n", e) time.sleep(60) break time.sleep(SLEEP_TIME)
More Web API Examples
If you would like to see more web API examples for Circuit Python visit the Adafruit_Requests example directory here.
There are very basic examples for:
- Discord
- Fitbit
- Github
- Mastodon
- Steam
- Twitch
- Youtube
- and more....