Web APIs
Contents
Web APIs#
In this lesson you will learn about web APIs and how to interact with in Python them using the requests module.
Table of Contents
Part 1: Setup#
First, you’ll need to install the requests module.
repl.it
Click the icon that looks like a box on the left Packages
. Search for
requests
then click the plus sign next to it to add the package.
otherwise
pip install requests
at the command line
Then create a new file named apis.py
and add a line import requests
at the
top. Also, we’re going to use the pprint
module today, so add the line below
to import that.
from pprint import pprint
import requests
Part 2: Introduction#
An API, or Application Programming Interface, is a server that you can use to retrieve and send data to using code. APIs are most commonly used to retrieve data, and that will be the focus of this beginner tutorial.
When we want to receive data from an API, we need to make a request. Requests are used all over the web. For instance, when you visited this page, your web browser made a request to the web server, which responded with the content of this web page.
API requests work in exactly the same way – you make a request to an API server for data, and it responds to your request.
Today we’ll be interacting with APIs in three ways:
From the browser
From the command line, using
curl
In Python using the
requests
module.
Part 3: How web requests work#
For our first experiment with APIs, I’ve added a file to my github account which you can view in your browser: hello.txt.
You should see the text:
Hello python class!
Now, you can do the same thing at the command line. At the shell, copy and paste the following:
curl "https://raw.githubusercontent.com/alissa-huskey/python-class/master/hello.txt"
Finally, we can do the same thing in Python.
from pprint import pprint
import requests
def request_demo():
"""Explore how web request work"""
url = "https://raw.githubusercontent.com/alissa-huskey/python-class/master/hello.txt"
response = requests.get(url)
# this shows us the body of the response
print(response.text)
request_demo()
Part 4: The parts of an API response#
The response that we get back from the server consists of three main parts:
status – a number that indicates if the request worked, and if not, what went wrong
headers – metadata about the content (like the size and type) and the connection
body – the content of the reply
We can use python to look at each of these. Modify your request_demo()
function.
def request_demo():
"""Explore how web request work"""
url = "https://raw.githubusercontent.com/alissa-huskey/python-class/master/hello.txt"
response = requests.get(url)
# this shows us the body of the response
print(response.text)
# this should show 200: the status code for SUCCESS
print(response.status_code)
# this converts the headers value into a dictionary for pprint
headers = dict(response.headers)
# here we can see the headers. For example, the
# 'Content-Length' tells us that the size is 40 bytes and
# 'Content-Type' tells us that it was plain text
pprint(headers)
Part 4.1: Solo Exercise - Print the Weather#
First, comment out the call to request_demo()
.
Write a new function called request_weather()
. Call the url http://wttr.in
then print response.text
. Be sure to call the new function!
Part 5: Getting data from an API#
Getting data from an API is exactly the same. The only difference is that the data is structured in a way to make it easy for code to work with.
Let’s do an exercise with a real API.
For this exercise we’ll be using the NASA astros API.
First, let’s see it in the browser: api.open-notify.org/astros.json.
Next, at the command line:
curl "http://api.open-notify.org/astros.json"
And finally, in Python. Make a new function request_astros()
. For now, it
will be just the same as request_demo()
, but with a different URL.
def request_astros():
"""Print out the astronauts currently in space using NASAs astros API
https://api.nasa.gov/
"""
url = "http://api.open-notify.org/astros.json"
response = requests.get(url)
# this shows us the body of the response
print(response.text)
Then comment out the line where we call request_demo()
and replace add a new
line to call request_astros()
.
# request_demo()
request_astros()
Part 5.1: What is JSON#
JSON stands for JavaScript Object Notation. It is the way that JavaScript represents data objects. It is the most common way for APIs to format their data because it is easy for programming languages to read and write.
Usually we would need to download a library to parse the text and convert it to
JSON. (The word “parse” basically means reading data of one form and
translating it to another, usable form.) But happily the requests API takes
care of that for us by providing the response.json()
function.
This returns an object that works just like the Python data type that is in the JSON. Most often, as in this case, APIs respond with a dictionary.
Part 5.2: Accessing JSON data#
Accessing elements works just the same way as it does with a normal dictionary
– using the syntax variable['key']
.
Let’s say we want to print out the number of people in space. Comment out the
print
line from before and add the last two lines here to the end of your
request_astros()
function:
def request_astros():
"""Print out the astronauts currently in space using NASAs astros API
https://api.nasa.gov/
"""
url = "http://api.open-notify.org/astros.json"
response = requests.get(url)
# this shows us the body of the response
# print(response.text)
data = response.json()
print(f"There are {data['number']} people in space today.")
Now let’s say we want to print out the names of our astronauts. Add the
following two lines to the end of your request_astros()
function:
def request_astros():
...
for astro in data['people']:
print(f"- {astro['name']}")
Part 5.3: Solo Exercise - Latitude and Longitude#
Write a new function called request_location()
.
Get the URL https://freegeoip.app/json/
. Print the
latitude
and longitude
from the response json()
.
Be sure to call your new function and comment out the old one.
Copy the resulting values and paste them into a comment in your script for future reference.
Part 6: Parameters#
Just like functions have arguments, APIs can have parameters
.
Part 6.1 Parameters in URLs#
(No code in this section, just going over some concepts. Feel free to play
around in curl
or your web browser though.)
The simplified syntax for a basic URL is something like:
http[s]://host/[path]
In its most basic form, this is called a resource. This means that it points
to something–a web site, an image, a program–sitting on a computer somewhere
so that people can access it though the network. This is where the acronym URL
comes from – Uniform Resource Locator. One kind of resource is the things you
pull up in your web browser every day, a web page
.
This simple form of URL works well for images and static web pages. But just
like the print
function would be rather less useful if we couldn’t pass it
the strings to print, so would the web be a good deal more dull if we didn’t
have a way to pass parameters.
Happily parameters can be passed as part of the URL. A ?
is used to separate
the resource from the parameters. Then the &
is used between parameters.
For example, to search google we need to submit a search query. Google uses
the parameter q
for the query. So if you felt like it you could skip the
search box and type directly into the URL bar:
http://google.com/search?q=urls
The wttr.in
service that we used before has an extensive query language
(which you can retrieve by calling wttr.in/:help
). A couple of examples:
http://wttr.in/moon?lang=de
http://wttr.in/SFO?lang=de&format=j1
(Much of the wttr.in
query language is fairly non-standard, so in some ways
it’s not the best example. On the other hand, it does show the flexibility.
Plus, it’s fun to play around with.)
Keep an eye on your URL bar over the next few days to see what jumps out at you. I use this knowledge often to bypass slow pages and occasionally to get around a bug.
Side note
In modern browsers you can usually type in spaces and other special
characters directly into the URL bar. After you hit enter you may notice
that your spaces have been changed to +
or %20
. The browser is URL
encoding it for you–that is, changing some characters so that they’ll be
understood correctly by the server on the other end.
Many modern APIs can handle spaces and other special characters too but some
only accept encoded values. The requests
module handles much of this for
you, but you may run into occasional problems when passing unencoded
parameters at the command line. If your curl requests are mysteriously
failing, try replacing any spaces with +
.
Part 6.2 API Parameters#
Now let’s put this knowledge to use to pass parameters to an API.
We talked about how the resources you access through the browser are known as
web pages
. API resources commonly referred to as endpoints
.
For this exercise, we’ll use the hellosalut API, which tells you how to say “hello” in a given language.
The endpoint
is https://hellosalut.stefanbohacek.dev/
. We’ll pass it the
lang
parameter, starting with the language code de
for German.
Step 1: Try it using curl
#
curl "https://hellosalut.stefanbohacek.dev/?lang=de"
Now we’ll do the same thing in Python using the requests
module. The
requests.get
function takes an optional argument params
which access a
hash.
Step 2: Add it to apis.py
#
def request_hello():
"""Say "hello" in another language using the hellosalut API
https://hellosalut.stefanbohacek.dev/
"""
url = "https://hellosalut.stefanbohacek.dev/"
response = requests.get(url, params={'lang': 'de'})
data = response.json()
# print(data)
print(data['hello'])
Side note
The syntax for sending params
is called keyword arguments
or
occasionally named arguments
. It refers to when an argument is passed
with name=value
instead of just the value
.
We’ve come across it before – the print
function has a keyword argument
end
that lets you change or remove the newline that is usually added. For
example: print("Your name: ", end="")
. This is useful when a function has a
bunch of arguments that are optional. Just like our get
function.
Part 6.3: Solo Exercise - Parameters#
Pick one of the following:
Get an activity from the Bored API
Endpoint URL:https://www.boredapi.com/api/activity
Parameters:participants
=1
Print:activity
Get insulted by the Evil Insult API
Endpoint URL:https://evilinsult.com/generate_insult.php
Parameters:lang
=en
,type
=json
Print:insult
Guess someone’s age using the Agify API
Endpoint URL:https://api.agify.io
Parameters:name
= your (or someone else’s) first name
Print:age
Part 7: Private Data#
Note
You’ll need your latitude
and longitude
values from Part 5.3.
If you don’t have them available run the request_location()
function again.
Here we’re going to take a little detour to learn how to safely use private data in code.
It is best practice to avoid storing private data, such as API keys, in GitHub
where our account could be hacked or the repo may someday become public.
Instead we’re going to create a private
module then add it to our git ignore
file.
Caution
Private data should not be stored on repl.it since all repls are public and can be forked by anyone. For the purposes of this lesson it’s acceptable since the data is not particularly sensitive.
Step 1: Create the private
module#
We have made our own modules before – back in our pypet battle project, we
made a pets
module. You may recall that a module is simply a python file that
can be imported.
So we’re going to add a private
module by creating a new file called
private.py
.
Most of this should be familiar – docstring and global variables. I’ll explain the unfamiliar bits soon.
"""Private data such as API keys that should not be committed in git"""
__all__ = ["LAT", "LNG"]
LAT = "34.06" # replace with your latitude
LNG = "-118.26" # replace with your longitude
Step 2: Ignore it in git#
In the same directory create or edit the file .gitignore
and add the filename to it.
private.py
Step 3: Import it#
See also
Now we need to import it. Add the following to the imports section (somewhere
near the top) of your apis.py
file.
from private import LAT, LNG
Part 8: Headers#
We’ve looked at headers that are received in the response already. We can also use headers in the request to provide information about the request we’re sending or about what we want back in the response.
A few of the more common standard request headers include:
Accept
: what kind of content we want back (its media-type), for exampletext/plain
orimage/png
Accept-Encoding
: how we want the response compressed, for examplegzip
oridentity
(uncompressed)Content-Type
: the media-type of the data we’re sending, for exampletext/json
orimage/jpeg
One of the most common use of headers is for authentication and that’s what
we’ll be doing today using the openuv.io
API.
Part 8.1: Get Your API Key#
Step 1: Sign up#
In your web browser visit openuv.io. Click on Get My API Key then sign into a Google account.
You will be directed to the API docs for the uv
endpoint. Under the
Authorisation
header, you should see the text:
To authorise your client just add your API Key _______ to "x-access-token"
header for each request.
Step 2: Add it to private.py
#
Copy the API key shown then add it to private.py
. Don’t forget to add
OPENUV_KEY
to the __all__
list.
"""Private data such as API keys that should not be committed"""
__all__ = ["LAT", "LNG", "OPENUV_KEY"]
LAT = "34.06"
LNG = "-118.26"
OPENUV_KEY = "" # your API key here
Step 3: Import it in apis.py
#
Then add OPENUV_KEY
to the import
statement in apis.py
.
from private import LAT, LNG, OPENUV_API
Part 8.2: Sending the x-access-token
Header#
Now that latitude, longitude, and API key imported from private.py
we can use
them in our request to openuv.io
.
The openuv API uses a custom header x-access-token
for authentication. We’ll
send it as part of the headers
argument in the get()
function.
def request_uv():
"""Print UV and Ozone info for today"""
response = requests.get(
"https://api.openuv.io/api/v1/uv",
params={'lat': LAT, 'lng': LNG},
headers={'x-access-token': KEY}
)
data = response.json()
print("UV Index:", data['result']['uv'])
print("Ozone:", data['result']['ozone'])
Part 8.3: Solo Exercise - RapidAPI#
RapidAPI is a platform and marketplace for APIs, so it’s a good resource for our lessons.
Sign up for an account on RapidAPI.
Choose one of the exercises below then click the link for your chosen exercise to open the relevant RapidAPI page.
If there is a blue
Subscribe to Test
button in the bottom center pane click it then click through the steps to subscribe to the free pricing plan.RapidAPI uses two headers for authentication:
x-rapidapi-key
andx-rapidapi-host
. Both values are shown in the middle pane underHeader Parameters
. Additional parameters will be listed below that underOptional Parameters
.Note: It is probably easier to copy the API key from the code samples right pane.
Exercise Options:
Use the Shakespere API to generate a random insult
Endpoint URL:
https://shakespeare1.p.rapidapi.com/shakespeare/generate/insult
Parameters:
limit
=1
Print:
contents
->taunts -> 0
Use the Random Cat Fact API to get a random cat fact
Endpoint URL:
https://random-cat-fact.p.rapidapi.com/
Print:
fact
Use the Population API to get a random quote
Endpoint URL:
https://get-population.p.rapidapi.com/population/country
Parameters:
country
="Mexico"
Print:
count
orreadable_format
Part 9: Request Methods#
So far we’ve been doing GET
requests. This is one of several HTTP request
methods.
The GET method is for retrieving (getting) data. Another example is the POST method which is for adding a new data record. Or the DELETE method which is for deleting data.
Here are the most commonly used request methods.
method |
r/w |
description |
---|---|---|
GET |
read |
retrieve data |
POST |
write |
add/create a new record |
either |
general processing |
|
PUT |
write |
replace all data for a specific record |
write |
create a new specific record |
|
PATCH |
write |
partial update to specific record |
DELETE |
write |
delete a specific record |
If you think of the resource as the noun, you can think of the request method as the verb–that is, the action to take.
Here is an imaginary address book API. In this example, the noun would be the contact(s)
.
endpoint |
method |
description |
---|---|---|
/contacts |
GET |
get a list of all contacts |
/contact/{id} |
GET |
get all of the details about a specific contact |
/contacts |
POST |
add a new contact |
/contact/{id} |
PUT |
create or replace all data for contact |
/contact/{id} |
PATCH |
update only the parts contact data that is submitted |
/contact/{id} |
DELETE |
delete the contact |
Part 9.1: Request methods in Python#
As you’ve already learned, you use the requests.get()
method to make a GET
request in Python.
The requests
module has methods that correspond to each of the other HTTP
request methods as well. So, to make a POST
method you would call
requests.post()
. To make a PUT
method, you call requests.put()
and so
on.
Whereas the requests.get()
method uses the params
keyword argument, the
requests.post()
, requests.put()
and requests.patch()
methods use the
data
keyword argument.
The jsonplaceholder API exists for the explicit purpose of making fake JSON requests or generating fake data.
I recommend taking a look at the Resources and Routes sections on that page and maybe checking out the guide to get an idea of how web APIs are sometimes structured.
For this exercise we’ll be using the jsonplaceholder API to add a fake to-do.
Add a new method request_todo()
.
def request_todo():
"""Use the jsonplaceholder API to add a fake to-do."""
response = requests.post(
"https://jsonplaceholder.typicode.com/todos",
data = {
'title': "laundry",
'userId': 1
}
)
if not response.ok:
print(f"ERROR: Request failed: {response.status_code} {response.reason}")
return
data = response.json()
print(f"SUCCESS Added new to-do ID: {data['id']}")
Part 9.2 POST
, PUT
and PATCH
#
The
POST
method is intended for requests that will add a new record to a collection of records. Endpoints should be plural, for example/contacts
or/contacts/42/addresses
.The
PUT
method is intended for requests that will replace the all of the data of a single, specific record. It may be used to create a new record if the record for that endpoint does not exist. Endpoints should be singular, for example/contact/42
or/contact/42/usernames/twitter
.The
PATCH
method is for requests that will update only the part(s) of a single, specific record that are included in the request data while leaving the rest of the record data alone. Endpoints should be singular, for example/contact/42
or/contact/42/addresses/work
.
The difference between PUT
and PATCH
can be confusing, so I’ll use our
imaginary contacts API to demonstrate.
First we’ll use the GET
method to take a look at an (imaginary) contact.
>>> import request
>>> from pprint import pprint
>>> response = requests.get(f"http://api.fake-contacts.com/contacts/1")
>>> print("Joe's contact info")
>>> pprint(response.json())
Joe's contact info
{
"id": 1,
"name": "Joe Smith",
"phone": "555-5555",
"email": "joe.smith@gfake.com"
}
Then we’ll use the PUT
method and submit a new phone
number and print the
updated data after another GET
request.
>>> response = requests.put(
>>> f"http://api.fake-contacts.com/contacts/{id}"
>>> data={'phone': "555-5556"}
>>> )
>>> response = requests.get(f"http://api.fake-contacts.com/contacts/1")
>>> print("Joe's contact info")
>>> pprint(response.json())
Joe's contact info
{
"id": 1,
"name": null,
"phone": "555-5556",
"email": null
}
Since the request only included the phone
data, all of the other fields
were replaced with null
(which is like Python’s None
). Some APIs might
instead refuse to process the update and respond with a 400 (Bad Request)
status if all expected data fields are not present in the request.
Here’s what it would have looked like if we had instead used a PATCH
request instead.
>>> response = requests.patch(
>>> f"http://api.fake-contacts.com/contacts/{id}"
>>> data={'phone': "555-5556"}
>>>)
>>> response = requests.get(f"http://api.fake-contacts.com/contacts/1")
>>> print("Joe's contact info")
>>> pprint(response.json())
Joe's contact info
{
"id": 1,
"name": "Joe Smith",
"phone": "555-5556",
"email": "joe.smith@fake.com"
}
Important
While I have described the intended purpose of each method according to both the HTTP specification and modern best practices, there is nothing in the technology to enforce this behavior. The behavior of any given API depends entirely on its implementation.
In reality, you’ll see all kinds of things–partial updates that respond to
POST
requests, endpoints that look like /update_phone
and respond to GET
requests, deletes that respond to POST
methods, and anything else you can
imagine.
You can’t count on APIs to behave according to best practices. Start by looking to an APIs documentation. Ultimately though, test its behavior yourself with dummy data before relying on it for anything you care about.
Part 9.3 Request methods using curl
#
Curl defaults to the GET
request methods for http[s]
requests but you can
also use the -X
or --request
flag to specify the request method.
For example:
curl -X DELETE "https://jsonplaceholder.typicode.com/todos/2"
You can also use the -d
or --data
flag to pass data along with the -H
or
--header
flag to specify the Content-Type
. For example, you could do the
same to-do
exercise from above using curl.
Side note: You can use
\
on the command line to break a command into multiple lines.
curl -X POST \
--data '{"title": "laundry", "userId": 1}' \
--header 'Content-Type: application/json' \
"https://jsonplaceholder.typicode.com/todos"
Exercise
Use the Pirate Translator API translate text into pirate-speak.
Endpoint URL: https://piratespeak.p.rapidapi.com/pirate.json
Method: POST
Data: text
= "Hello friend."
Print: contents
-> translated
Sidenote: You may have noticed that this API does not conform to the practice of the
POST
method being used for adding new data. Another common use of thePOST
method is for cases when a large amount of data may need to be submitted, due to browser and web server limits on URI length.
Part 10. Final Project#
Use the Trello API to build a personal Trello CLI.
Phase 1: Print the open cards from your To Do
list.#
Sign into your Trello account and get an API key and token by visiting trello.com/app-key.
Each API request will use:
Base URL:
https://api.trello.com/1
Params:
key
andtoken
Find the
id
of yourTo Do
list.Use the
/members/id/boards
endpoint and the special memberid
ofme
to get a list of your open boards.Filter to only open boards by passing the
filter
parameter with the valueopen
.Include the board
lists
in the results by passing thelists
param with the valueopen
.Find your board with the
name
QCC: your name
, then the list with thename
To Do
on that board. Save theid
of that list.
Get the cards on your
To Do
list.Use the
/lists/id/cards
endpoint.Filter to only open cards by passing the
cards
parameter with the valuevisible
.
Print the card info.
For each card:
Optional: Skip any that you are not a member of by checking if
subscribed
istrue
.Print the card
name
,shortUrl
,due
date andlabels
name
.
Phase 2: Show card details#
Use the
enumerate()
function to print a number next to each card name.After printing the list of cards, get input from the user.
If they enter a number of a listed card, print the
desc
, anychecklist
items and any other card details you would like to see for the chosen card.
If they enter
q
(or some other command for quit) exit the program.
Phase 3: Card actions#
After printing the card list, provide a menu of card actions that a user can take on a card, giving each a letter or command word.
Show card: print card info (from Phase 2)
Mark as in-progress: move the card to the
In-Progress
list.Mark as Done: move card to the
Done
list and setdueComplete
totrue
.
If the user includes the letter or command word in the input, use the API to take the appropriate action for that card.
Phase 4: More card views#
Provide a way for the user to optionally view more or different card views. For example:
Make the default view a dashboard that prints a simplified list of cards from each of the
Done
,In-Progress
,To Do
andComing Up
lists.Add an option to specify which list(s) to include. (For example,
In-Progress
andTo Do
.)Add a search option, that allows the user to specify a part of a name and/or description to search for.
Allow the user to filter cards by fields, such as the
label
(s), if you aresubscribed
,due
date (overdue, due soon), etc.
More Info
This will be a medium-term project.
I expect the project to follow the requirements for Phase 1 fairly closely and it is the only one that I provided detailed instructions for.
From there on, you will need to dig through the API docs to figure out the best way to accomplish your goals.
You can also consider the rest more guidelines or suggestions. The goal is to make a tool that you will use on a regular basis. Focus on what will make it most useful to make you more productive.
Tips and Reminders
:class: tip
Choose a goal for each week. Phase 1 should be doable in a week or maybe two. After that, your goal choices will be your own. Consider posting your goal for the week to discord to help keep you on track.
Break your immediate work into bite-sized pieces. Think of the easiest thing you can do that will mean making a step forward with tangible, visible progress. If it doesn’t feel almost too easy, figure out how to break it down further.
Save your progress and run your script frequently after any change, even when the change seem insignificant. You want to catch errors and unexpected results as soon as they happen.
Don’t be shy about asking for help from me or your fellow classmates. Everyone gets stuck sometimes. Don’t let yourself spiral or stall.
I suggest starting a new repo for this project.
Commit and push frequently, at a minimum at the end of each coding session, ideally after any feature addition, behavior change, or refactor (code improvement that does not change the behavior). Review the diff of your changes before each commit, as well as at the beginning of every coding session. (Hopefully there are none for the latter most of the time.)
Consider adding your own Trello cards for the next bite-size step or two. You could even make a new board if you’d like. But be careful not to spend too much time planning your project–you should be spending no more than a few minutes a day gardening your tasks/cards/whatever. And don’t plan too far ahead–your goals and plans will change as the project matures, so planning too far into the future is counter-productive.
Consider a DEBUG mode that can print out additional details to help with debugging.
Give me the opportunity to review your code when you’re done with any major step. Perhaps also give your classmate(s) an opportunity to review your code as well. Code reviews are a good way to get feedback and course-correct early, as well as learning from each other.
See Also#
See also
Requests: HTTP for Humans - docs for the Python
requests
module