Working with a REST API in Python

Introduction

In this article you will learn how to write requests to a REST API in Python 3.

If your goal is creating your own REST API, go to the article «Flask»

Before installing anything, make sure you are familiar with working in a virtual environment Python.

You can read about this in the article «Virtual environments in Python»

Preparation

Activate your virtual environment and install requests with the command

python3 -m pip install requests

Examine the list of installed modules

python3 -m pip list

Package Version ---------- --------- certifi 2020.6.20 chardet 3.0.4 idna 2.10 pip 20.2.3 requests 2.24.0 setuptools 50.3.1 urllib3 1.25.10 wheel 0.35.1

requests pulls in certifi, chardet, idna and urllib3 along with it

You can check where requests was installed in this environment with the command

python3 -m pip show requests

Name: requests Version: 2.24.0 Summary: Python HTTP for Humans. Home-page: https://requests.readthedocs.io Author: Kenneth Reitz Author-email: me@kennethreitz.org License: Apache 2.0 Location: /home/andrei/python/virtualenvs/answerit_env/lib/python3.8/site-packages Requires: certifi, chardet, urllib3, idna Required-by:

GET

To make a GET request it is enough to import requests and execute requests.get

Create the file rdemo.py with the following content:

import requests r = requests.get('https:/xkcd.com/353/') print(r)

Run the script with the command

python3 rdemo.py

<Response [200]>

If you got 200, all is well. Let's change our code to find out what actions we can perform on the <Response [200]> object

dir(r) will give a list of available attributes and methods

import requests r = requests.get('https:/topbicycle.ru/b/stels_pilot_950_md_26.php') print(dir(r))

['__attrs__', '__bool__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_content', '_content_consumed', '_next', 'apparent_encoding', 'close', 'connection', 'content', 'cookies', 'elapsed', 'encoding', 'headers', 'history', 'is_permanent_redirect', 'is_redirect', 'iter_content', 'iter_lines', 'json', 'links', 'next', 'ok', 'raise_for_status', 'raw', 'reason', 'request', 'status_code', 'text', 'url']

More detailed information can be obtained by replacing dir(r) with help(r)

If you are interested, read the article requests help

From this document you can learn that the http status is contained in the attribute status_code

import requests r = requests.get('https:/topbicycle.ru/b/stels_pilot_950_md_26.php') print(r.status_code)

If everything went well, you will get

200

ok returns True if the response is not 4XX or 5XX

headers returns the response header

Also from this document you can learn that text returns the response body in unicode format, while content is the response body in bytes

Change the code to

import requests r = requests.get('https:/xkcd.com/353/') print("content:") print("-----") print(r.content) print("-----") print("text:") print("-----") print(r.text)

And study the difference

content is usually used to work with images

Go to TopBicycle.ru and find the first photo of the bicycle.

Copy its url

https://topbicycle.ru/b/img/stels_pilot_950_MD_26.jpg

Now change the code to save the image to a file

import requests r = requests.get(https://topbicycle.ru/b/img/stels_pilot_950_MD_26.jpg") with open("bike.jpg", "wb") as f: f.write(r.content)

If everything went well, the following photo will appear in your folder

stels pilot 950 md 26 folded, image from www.TopBicycle.ru
Photo ©: TopBicycle.ru / shot on Samsung

GET with parameters

To test your REST API skills you can use the website httpbin.org

It will send your request back to you.

Let's change the code rdemo.py :

Parameters can be written right after the url following a question mark, but it is more reliable to put them in a separate dictionary.

import requests payload = {'page': 2, 'count': 25} r = requests.get(https://httpbin.org/get", params=payload) print(f"url: {r.url} \n\ntext: \n {r.text}")

python3 rdemo.py

url: https://httpbin.org/get?page=2&count=25 text: { "args": { "count": "25", "page": "2" }, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Host": "httpbin.org", "User-Agent": "python-requests/2.24.0", "X-Amzn-Trace-Id": "Root=1-5f8c2d50-4f48f16c0991ad0e6e45676e" }, "origin": "87.92.8.47", "url": "https://httpbin.org/get?page=2&count=25" }

POST

To send and test a POST request, make a few small changes to the code.

Obviously, GET must be replaced with POST, and params must be replaced with data

import requests payload = {'website': 'heihei.ru', 'established': 2018} r = requests.post('https://httpbin.org/post', data=payload) print(f"url: {r.url} \n\ntext: \n {r.text}")

url: https://httpbin.org/post text: { "args": {}, "data": "", "files": {}, "form": { "established": "2018", "website": "heihei.ru" }, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Content-Length": "34", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "python-requests/2.24.0", "X-Amzn-Trace-Id": "Root=1-5f8c30ac-6b32899f073f9df4055c55c4" }, "json": null, "origin": "87.92.8.47", "url": "https://httpbin.org/post" }

The responses we received from httpbin came in json format. When working with json it is often convenient to use the built-in .json() method

In this case the data is written into a dictionary and is very easy to access.

Note the "form" data that was sent is returned in this field.

Let's change the code so that only this data is printed to the screen

import requests payload = {'website': 'heihei.ru', 'established': 2018} r = requests.post('https://httpbin.org/post', data=payload) r_dict = r.json() print(r_dict['form'])

python3 rdemo.py

{'established': '2018', 'website': 'heihei.ru'}

PUT

Everything is the same as with POST, just replace post with put

Let's look at an example where you need to send json in the request body and also use an existing token for authorization

import requests import json url = "http://eth1.ru" new_access_token = sdlfjsljglkjfd;lkgjdlkhjlkjgdlkhjlkdjglkj body = """[{"id":"1234abc","name":"andrei","website":"eth1.ru"}]""" payload = json.loads(body) json_headers = {"Content-type": "application/json", "Authorization": "Bearer %s" %new_access_token } r = requests.put(url, headers=json_headers, json=payload, verify=False)

Pay attention to the following points

Handling the response

Let's look at techniques that will come in handy when working with the received data

Suppose data has been received in the format json.

We need to extract from it the token, which is stored in access_token

To get it, use the json() method, which returns a dictionary (<class 'dict'>).

And from this dictionary get the token by the access_token key

r_dict = r.json() access_token = r_dict["access_token"]

json.dumps

If you want to examine the received json right away, you can use the method dumps()

import json print(json.dumps(response.json(), indent=4))

If you got a dict rather than json, json.dumps must be used like this:

import json print(json.dumps(resp.dict, indent=4, sort_keys=True))

sort_keys is optional. I am just showing it as an example, to demonstrate that the keys can be sorted.

Extracting part of the response

Often it is not the whole response that is of interest, but only a part. Read about how to extract it correctly from the response in the article

«How to access nested objects in json»

Authentication

Let's look at basic authentication on the httpbin website

Make up any username and a password for it.

I made up andrey with the password heihei

Go to httpbin.org . Make sure the address bar contains basic-auth/andrey/heihei or the login and password you made up.

Authentication image from website www.aredel.com

Enter your login and password

Authentication image from website www.aredel.com

Make sure the authentication succeeded

Authentication image from website www.aredel.com
If you need a new laptop, visit the website Нотик.ру

Now let's do the same authentication with Python

Create the file auth_demo.py with the following code

import requests r = requests.get('https://httpbin.org/basic-auth/andrey/heihei', auth=('andrey', 'heihei') ) print(r.text)

python3 auth_demo.py

{ "authenticated": true, "user": "andrey" }

The response matches what we already received in the browser

Make the same request, but with a wrong password. Make sure that text contains nothing. Replace print(r.text) with print(r) and make sure the object received is

<Response [401]>

Delay

It is often necessary to limit the response waiting time. This can be done with the timeout parameter

Go to httpbin.org the section - / #/ Dynamic_data / delete_delay__delay_ and study the documentation - if you make a request to this url you can set the time after which the response will be sent.

Create the file timeout_demo.py with the following content

import requests r = requests.get('https://httpbin.org/delay/1', timeout=3) print(r)

The delay is one second. And the response can be awaited for up to three seconds.

python3 timeout_demo.py

<Response [200]>

Change the code so that the response arrives definitely later than our three-second timeout.

import requests r = requests.get('https://httpbin.org/delay/7', timeout=3) print(r)

The delay is seven seconds. And the response can still be awaited for only up to three seconds.

python3 timeout_demo.py

Traceback (most recent call last): File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 421, in _make_request six.raise_from(e, None) File "<string>", line 3, in raise_from File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 416, in _make_request httplib_response = conn.getresponse() File "/usr/lib/python3.8/http/client.py", line 1347, in getresponse response.begin() File "/usr/lib/python3.8/http/client.py", line 307, in begin version, status, reason = self._read_status() File "/usr/lib/python3.8/http/client.py", line 268, in _read_status line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") File "/usr/lib/python3.8/socket.py", line 669, in readinto return self._sock.recv_into(b) File "/usr/lib/python3/dist-packages/urllib3/contrib/pyopenssl.py", line 326, in recv_into raise timeout("The read operation timed out") socket.timeout: The read operation timed out During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/usr/lib/python3/dist-packages/requests/adapters.py", line 439, in send resp = conn.urlopen( File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 719, in urlopen retries = retries.increment( File "/usr/lib/python3/dist-packages/urllib3/util/retry.py", line 400, in increment raise six.reraise(type(error), error, _stacktrace) File "/usr/lib/python3/dist-packages/six.py", line 703, in reraise raise value File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 665, in urlopen httplib_response = self._make_request( File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 423, in _make_request self._raise_timeout(err=e, url=url, timeout_value=read_timeout) File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 330, in _raise_timeout raise ReadTimeoutError( urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host='httpbin.org', port=443): Read timed out. (read timeout=3) During handling of the above exception, another exception occurred: Traceback (most recent call last): File "timeout_demo.py", line 4, in <module> r = requests.get('https://httpbin.org/delay/7', timeout=3) File "/usr/lib/python3/dist-packages/requests/api.py", line 75, in get return request('get', url, params=params, **kwargs) File "/usr/lib/python3/dist-packages/requests/api.py", line 60, in request return session.request(method=method, url=url, **kwargs) File "/usr/lib/python3/dist-packages/requests/sessions.py", line 533, in request resp = self.send(prep, **send_kwargs) File "/usr/lib/python3/dist-packages/requests/sessions.py", line 646, in send r = adapter.send(request, **kwargs) File "/usr/lib/python3/dist-packages/requests/adapters.py", line 529, in send raise ReadTimeout(e, request=request) requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='httpbin.org', port=443): Read timed out. (read timeout=3)

If you are not thrilled by such exception handling, change the code using try except

import requests try: r = requests.get('https://httpbin.org/delay/7', timeout=3) except (requests.exceptions.Timeout, requests.exceptions.ConnectionError ) as err: print('Response is taking too long.') else: print(r)

python3 timeout_demo.py

Response is taking too long.

Article author: Andrei Olegovich