Requests: Продвинутое использование

Объекты Session

Объект Session позволяет сохранять определённые настройки между запросами. Он автоматически сохраняет cookies для всех запросов, выполненных через данный экземпляр, и использует пул соединений из библиотеки urllib3. Это означает, что при выполнении нескольких запросов к одному серверу одно и то же TCP-соединение будет переиспользовано, что может существенно повысить производительность.

Объект Session предоставляет все методы основного API Requests.

Давайте продемонстрируем сохранение cookies между запросами:

s = requests.Session()

s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')
r = s.get('https://httpbin.org/cookies')

print(r.text)
# '{"cookies": {"sessioncookie": "123456789"}}'

Сессии можно также использовать для задания значений по умолчанию при выполнении запросов. Для этого достаточно установить соответствующие свойства объекта сессии:

s = requests.Session()
s.auth = ('user', 'pass')
s.headers.update({'x-test': 'true'})

# в запросе будут отправлены оба заголовка: 'x-test' и 'x-test2'
s.get('https://httpbin.org/headers', headers={'x-test2': 'true'})

Любой словарь, передаваемый в метод запроса, объединяется с параметрами, заданными на уровне сессии, при этом параметры, указанные непосредственно в методе, имеют приоритет над значениями сессии.

Однако имейте в виду, что параметры, переданные в методе, не сохраняются между вызовами, даже если используется сессия. В следующем примере cookies отправляются только в первом запросе, но не во втором:

s = requests.Session()

r = s.get('https://httpbin.org/cookies', cookies={'from-my': 'browser'})
print(r.text)
# '{"cookies": {"from-my": "browser"}}'

r = s.get('https://httpbin.org/cookies')
print(r.text)
# '{"cookies": {}}'

Если вам нужно вручную добавить cookies в сессию, воспользуйтесь функциями-утилитами для работы с cookies (Cookie utility functions) для управления свойством Session.cookies.

Сессии можно использовать и как менеджеры контекста:

with requests.Session() as s:
    s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')

При этом сессия будет закрыта сразу после выхода из блока with, даже если возникнут исключения.

Объекты Request и Response

При вызове методов, таких как requests.get() и других, происходит две основные операции. Сначала создаётся объект Request, который отправляется на сервер для получения ресурса. Затем, когда сервер отвечает, создаётся объект Response, содержащий всю информацию, возвращённую сервером, а также исходный объект Request, который вы сформировали. Ниже приведён пример запроса к серверам Wikipedia для получения полезной информации:

>>> r = requests.get('https://en.wikipedia.org/wiki/Monty_Python')

Чтобы увидеть заголовки, полученные от сервера, выполните:

>>> r.headers
{'content-length': '56170', 'x-content-type-options': 'nosniff', 'x-cache': 'HIT from cp1006.eqiad.wmnet, MISS from cp1010.eqiad.wmnet', 'content-encoding': 'gzip', 'age': '3080', 'content-language': 'en', 'vary': 'Accept-Encoding,Cookie', 'server': 'Apache', 'last-modified': 'Wed, 13 Jun 2012 01:33:50 GMT', 'connection': 'close', 'cache-control': 'private, s-maxage=0, max-age=0, must-revalidate', 'date': 'Thu, 14 Jun 2012 12:59:39 GMT', 'content-type': 'text/html; charset=UTF-8', 'x-cache-lookup': 'HIT from cp1006.eqiad.wmnet:3128, MISS from cp1010.eqiad.wmnet:80'}

Если же вам нужны заголовки, отправленные вами на сервер, обратитесь к атрибуту request:

>>> r.request.headers
{'Accept-Encoding': 'identity, deflate, compress, gzip',
 'Accept': '*/*',
 'User-Agent': 'python-requests/1.2.0'}

Подготовленные запросы

При получении объекта Response от API или метода сессии атрибут request представляет собой объект PreparedRequest, который был использован при отправке запроса. Иногда возникает необходимость внести дополнительные изменения в тело или заголовки запроса перед его отправкой. Пример такого подхода выглядит следующим образом:

from requests import Request, Session

s = Session()

req = Request('POST', url, data=data, headers=headers)
prepped = req.prepare()

# изменить тело запроса
prepped.body = 'Нет, я хочу именно эти данные в теле запроса.'

# изменить заголовки запроса
del prepped.headers['Content-Type']

resp = s.send(prepped,
    stream=stream,
    verify=verify,
    proxies=proxies,
    cert=cert,
    timeout=timeout
)

print(resp.status_code)

Если специальных изменений не требуется, вы можете сразу подготовить запрос, изменить объект PreparedRequest и отправить его с теми же параметрами, что и при вызове методов requests.* или Session.*.

Однако такой подход теряет некоторые преимущества сессии — например, настройки на уровне сессии, такие как cookies, не будут применены. Чтобы получить объект PreparedRequest с учётом состояния сессии, замените вызов метода Request.prepare() на Session.prepare_request(), как показано ниже:

from requests import Request, Session

s = Session()
req = Request('GET', url, data=data, headers=headers)

prepped = s.prepare_request(req)

# изменить тело запроса
prepped.body = 'Серьёзно, отправьте именно эти байты.'

# добавить заголовок
prepped.headers['Keep-Dead'] = 'parrot'

resp = s.send(prepped,
    stream=stream,
    verify=verify,
    proxies=proxies,
    cert=cert,
    timeout=timeout
)

print(resp.status_code)

Учтите, что подготовленные запросы не учитывают настройки окружения. Это может вызвать проблемы, если вы полагаетесь на переменные окружения для настройки Requests. Например, самоподписанные SSL-сертификаты, указанные в REQUESTS_CA_BUNDLE, не будут приниматься, что приведёт к ошибке SSL: CERTIFICATE_VERIFY_FAILED. Эту проблему можно обойти, явно объединив настройки окружения с параметрами сессии:

from requests import Request, Session

s = Session()
req = Request('GET', url)

prepped = s.prepare_request(req)

# Объединяем настройки окружения с параметрами сессии
settings = s.merge_environment_settings(prepped.url, {}, None, None, None)
resp = s.send(prepped, **settings)

print(resp.status_code)

Проверка SSL-сертификатов

Requests, как и браузер, проверяет SSL-сертификаты для HTTPS-запросов. По умолчанию проверка включена, и если сертификат не проходит проверку, генерируется исключение SSLError:

>>> requests.get('https://requestb.in')
requests.exceptions.SSLError: hostname 'requestb.in' doesn't match either of '*.herokuapp.com', 'herokuapp.com'

На этом домене SSL не настроен, поэтому выбрасывается исключение. В то же время GitHub настроен правильно:

>>> requests.get('https://github.com')
<Response [200]>

Вы можете указать параметру verify путь к файлу CA_BUNDLE или директории с сертификатами доверенных центров сертификации:

>>> requests.get('https://github.com', verify='/path/to/certfile')

или установить его на уровне сессии:

s = requests.Session()
s.verify = '/path/to/certfile'
Если параметр verify указывает на директорию, она должна быть обработана утилитой c_rehash, входящей в пакет OpenSSL.

Список доверенных центров сертификации также можно задать через переменную окружения REQUESTS_CA_BUNDLE. Если она не установлена, используется CURL_CA_BUNDLE в качестве запасного варианта.

Requests может игнорировать проверку SSL-сертификата, если установить параметр verify в False:

>>> requests.get('https://kennethreitz.org', verify=False)
<Response [200]>

При этом Requests принимает любой TLS-сертификат, предоставленный сервером, игнорируя несовпадение имени хоста и/или истёкшие сертификаты, что делает приложение уязвимым для атак «человек посередине» (MitM). Использование verify=False может быть оправдано при локальной разработке или тестировании.

По умолчанию параметр verify равен True и применяется только к сертификатам сервера.

Сертификаты на стороне клиента

Вы можете указать локальный сертификат для использования в качестве клиентского – как в виде одного файла (содержащего закрытый ключ и сертификат), так и в виде кортежа путей к этим файлам:

>>> requests.get('https://kennethreitz.org', cert=('/path/client.cert', '/path/client.key'))
<Response [200]>

или установить его на уровне сессии:

s = requests.Session()
s.cert = '/path/client.cert'

Если указан неверный путь или недопустимый сертификат, генерируется исключение SSLError:

>>> requests.get('https://kennethreitz.org', cert='/wrong_path/client.pem')
SSLError: [Errno 336265225] _ssl.c:347: error:140B0009:SSL routines:SSL_CTX_use_PrivateKey_file:PEM lib
Закрытый ключ клиентского сертификата должен быть незащищённым, так как Requests не поддерживает использование зашифрованных ключей.

Сертификаты центров сертификации (CA)

Requests использует сертификаты из пакета certifi, что позволяет пользователям обновлять список доверенных сертификатов без изменения версии Requests.

До версии 2.16 Requests поставлялся с набором корневых сертификатов из Mozilla trust store, обновляемым один раз для каждой версии. Если certifi не установлен, при использовании старых версий Requests мог использоваться устаревший набор сертификатов.

В целях безопасности рекомендуется регулярно обновлять пакет certifi!

Обработка содержимого тела ответа

По умолчанию тело ответа загружается целиком сразу после выполнения запроса. Вы можете изменить это поведение и отложить загрузку содержимого до момента обращения к атрибуту Response.content, указав параметр stream=True:

tarball_url = 'https://github.com/psf/requests/tarball/main'
r = requests.get(tarball_url, stream=True)

На данном этапе загружены только заголовки, а соединение остаётся открытым, что позволяет по условию загрузить содержимое, например:

if int(r.headers['content-length']) < TOO_LONG:
    content = r.content
    ...

Дальнейший контроль над процессом загрузки можно осуществлять с помощью методов Response.iter_content() и Response.iter_lines(). Либо можно напрямую прочитать необработанное тело из объекта urllib3.HTTPResponse, доступного через Response.raw.

Если вы используете параметр stream=True, Requests не вернёт соединение в пул до тех пор, пока не будут считаны все данные или не вызовется метод Response.close. Поэтому, если вы планируете частично читать тело или не читать его вовсе, рекомендуется выполнять запрос в блоке with для гарантированного закрытия соединения:

with requests.get('https://httpbin.org/get', stream=True) as r:
    # Обработка ответа

Поддержка постоянного соединения (Keep-Alive)

Благодаря urllib3, постоянные соединения (keep-alive) автоматически поддерживаются в рамках одной сессии. Все запросы, выполняемые через сессию, будут использовать одно и то же соединение!

Учтите, что соединение возвращается в пул для повторного использования только после полного считывания тела ответа. Поэтому убедитесь, что либо stream равен False, либо вы полностью считали свойство content объекта Response.

Потоковая отправка данных (Streaming Uploads)

Requests поддерживает потоковую отправку данных, позволяя отправлять большие файлы или потоки без их полной загрузки в память. Для этого достаточно передать в качестве тела запроса файловый объект:

with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)
Рекомендуется открывать файлы в бинарном режиме, так как Requests может автоматически установить заголовок Content-Length равным количеству байт в файле. При открытии файла в текстовом режиме могут возникнуть ошибки.

Запросы с чанковым кодированием (Chunk-Encoded Requests)

Requests поддерживает передачу данных с чанковым кодированием (Chunked transfer encoding) для входящих и исходящих запросов. Чтобы отправить запрос с таким кодированием, достаточно передать генератор (или любой итератор без фиксированной длины) в качестве тела запроса:

def gen():
    yield 'hi'
    yield 'there'

requests.post('http://some.url/chunked', data=gen())

Для чтения ответов с чанковым кодированием рекомендуется итерироваться по данным с помощью метода Response.iter_content(). Лучше всего в этом случае установить stream=True и вызывать iter_content с параметром chunk_size=None. Если требуется ограничить максимальный размер чанка, можно указать параметр chunk_size равным нужному числу.

Отправка нескольких файлов с кодированием multipart

Вы можете отправлять несколько файлов в одном запросе. Например, если вам нужно загрузить изображения через HTML-форму с полем для выбора нескольких файлов с именем "images":

<input type="file" name="images" multiple="true" required="true"/>

Для этого достаточно передать параметр files в виде списка кортежей, где каждый кортеж имеет вид (имя_поля, информация_о_файле):

>>> url = 'https://httpbin.org/post'
>>> multiple_files = [
...     ('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
...     ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))]
>>> r = requests.post(url, files=multiple_files)
>>> r.text
{
  ...
  'files': {'images': 'data:image/png;base64,iVBORw ....'},
  'Content-Type': 'multipart/form-data; boundary=3131623adb2043caaeb5538cc7aa0b3a',
  ...
}
Рекомендуется открывать файлы в бинарном режиме, поскольку Requests может автоматически установить заголовок Content-Length, равный количеству байт в файле. При открытии файла в текстовом режиме могут возникнуть ошибки.

Хуки событий (Event Hooks)

Requests предоставляет систему хуков, позволяющую модифицировать процесс запроса или обрабатывать определённые события.

Доступный хук:

response: Объект ответа, полученный в результате запроса.

Вы можете назначить функцию-обработчик для конкретного запроса, передав словарь вида {hook_name: callback_function} в параметр hooks запроса:

hooks = {'response': print_url}

Эта функция получит объект ответа в качестве первого аргумента.

def print_url(r, *args, **kwargs):
    print(r.url)

Функция-обработчик должна самостоятельно обрабатывать исключения, так как необработанные ошибки не подавляются и будут выброшены вызывающему коду.

Если функция-обработчик возвращает значение, оно заменяет исходные данные; если ничего не возвращает, изменений не происходит.

def record_hook(r, *args, **kwargs):
    r.hook_called = True
    return r

Давайте выведем URL запроса во время его выполнения:

>>> requests.get('https://httpbin.org/', hooks={'response': print_url})
https://httpbin.org/
<Response [200]>

Вы также можете назначить несколько хуков для одного запроса. Например, чтобы одновременно вызвать две функции-хука:

>>> r = requests.get('https://httpbin.org/', hooks={'response': [print_url, record_hook]})
>>> r.hook_called
True

Хуки можно также задавать на уровне объекта сессии — все назначенные хуки будут вызываться для каждого запроса, выполненного через эту сессию. Например:

>>> s = requests.Session()
>>> s.hooks['response'].append(print_url)
>>> s.get('https://httpbin.org/')
https://httpbin.org/
<Response [200]>

Объект сессии может содержать несколько хуков, вызываемых в том порядке, в котором они были добавлены.

Пользовательская аутентификация

Requests позволяет реализовывать собственные механизмы аутентификации.

Любая функция, переданная в параметр auth метода запроса, получает возможность модифицировать запрос перед его отправкой.

Реализации аутентификации создаются как подклассы AuthBase <requests.auth.AuthBase>, что довольно просто. Requests предоставляет две стандартные реализации в модуле requests.auth: HTTPBasicAuth <requests.auth.HTTPBasicAuth> и HTTPDigestAuth <requests.auth.HTTPDigestAuth>.

Предположим, что у нас есть веб-сервис, который отвечает только если в заголовке X-Pizza указано верное значение. Для примера реализуем такую аутентификацию.

from requests.auth import AuthBase

class PizzaAuth(AuthBase):
    """Добавляет аутентификацию HTTP Pizza к запросу."""
    def __init__(self, username):
        # Инициализация необходимых данных
        self.username = username

    def __call__(self, r):
        # Модифицируем запрос, добавляя нужный заголовок
        r.headers['X-Pizza'] = self.username
        return r

Теперь можно выполнить запрос с использованием нашей аутентификации Pizza:

>>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))
<Response [200]>

Потоковые запросы

С помощью метода Response.iter_lines() можно легко обрабатывать потоковые API, например, Twitter Streaming API. Просто установите параметр stream=True и итерируйтесь по строкам ответа:

import json
import requests

r = requests.get('https://httpbin.org/stream/20', stream=True)

for line in r.iter_lines():
    # Пропускаем пустые строки (keep-alive)
    if line:
        decoded_line = line.decode('utf-8')
        print(json.loads(decoded_line))

При использовании параметра decode_unicode=True с методами Response.iter_lines() или Response.iter_content() рекомендуется задать запасную кодировку, если сервер её не укажет:

r = requests.get('https://httpbin.org/stream/20', stream=True)

if r.encoding is None:
    r.encoding = 'utf-8'

for line in r.iter_lines(decode_unicode=True):
    if line:
        print(json.loads(line))

Метод Response.iter_lines() не является потокобезопасным. Если вы вызываете его несколько раз, часть данных может быть потеряна. Чтобы избежать этого, сохраните итератор, например:

lines = r.iter_lines()
# Сохраните первую строку или пропустите её
first_line = next(lines)
for line in lines:
    print(line)

Прокси

Если необходимо использовать прокси, можно задать их для отдельного запроса через параметр proxies любого метода запроса:

import requests

proxies = {
  'http': 'http://10.10.1.10:3128',
  'https': 'http://10.10.1.10:1080',
}

requests.get('http://example.org', proxies=proxies)

Также можно настроить прокси для всей сессии, используя объект Session <requests.Session>:

import requests

proxies = {
  'http': 'http://10.10.1.10:3128',
  'https': 'http://10.10.1.10:1080',
}
session = requests.Session()
session.proxies.update(proxies)

session.get('http://example.org')
Настройка параметра session.proxies может работать не так, как ожидается: значения, заданные в сессии, будут переопределены настройками прокси из переменных окружения (возвращаемыми функцией urllib.request.getproxies). Чтобы гарантировать использование прокси, явно указывайте параметр proxies для каждого запроса.

Если настройки прокси не переопределены для отдельного запроса, Requests использует стандартные переменные окружения: http_proxy, https_proxy, no_proxy и all_proxy (также их варианты в верхнем регистре). Например, можно задать:

export HTTP_PROXY="http://10.10.1.10:3128"
export HTTPS_PROXY="http://10.10.1.10:1080"
export ALL_PROXY="socks5://10.10.1.10:3434"

python
>>> import requests
>>> requests.get('http://example.org')

Чтобы использовать HTTP Basic Auth для прокси, применяйте синтаксис http://user:password@host/ в соответствующих переменных окружения:

export HTTPS_PROXY="http://user:pass@10.10.1.10:1080"
python
proxies = {'http': 'http://user:pass@10.10.1.10:3128/'}
Хранение конфиденциальной информации (имён пользователей и паролей) в переменных окружения или файлах под контролем версий — серьёзный риск безопасности.

Чтобы задать прокси для конкретной схемы и хоста, используйте ключ в формате scheme://hostname. Это правило применяется ко всем запросам с указанной схемой и точным именем хоста.

proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'}

Обратите внимание, что URL прокси должны содержать схему.

Кроме того, использование прокси для HTTPS-соединений обычно требует, чтобы ваша машина доверяла корневому сертификату прокси. По умолчанию список сертификатов, которым доверяет Requests, можно получить так:

from requests.utils import DEFAULT_CA_BUNDLE_PATH
print(DEFAULT_CA_BUNDLE_PATH)

Вы можете переопределить этот пакет, установив переменную окружения REQUESTS_CA_BUNDLE (или CURL_CA_BUNDLE) с указанием другого пути к файлу:

export REQUESTS_CA_BUNDLE="/usr/local/myproxy_info/cacert.pem"
export https_proxy="http://10.10.1.10:1080"

python
>>> import requests
>>> requests.get('https://example.org')

SOCKS-прокси

Помимо обычных HTTP-прокси, Requests поддерживает SOCKS-прокси. Эта функция является дополнительной и требует установки сторонних библиотек.

Установите необходимые зависимости через pip:

python -m pip install requests[socks]

После установки использование SOCKS-прокси ничем не отличается от HTTP-прокси:

proxies = {
    'http': 'socks5://user:pass@host:port',
    'https': 'socks5://user:pass@host:port'
}

Схема socks5 означает, что DNS-разрешение производится на клиенте, а не на прокси-сервере. Если требуется, чтобы разрешение выполнялось на прокси, используйте схему socks5h.

Соответствие стандартам

Requests стремится соответствовать всем актуальным спецификациям и RFC, если это не создаёт неудобств для пользователей. Такое внимание к стандартам может приводить к поведению, которое может показаться необычным для тех, кто не знаком с соответствующими нормами.

Кодировки

При получении ответа Requests пытается определить кодировку для декодирования содержимого при обращении к атрибуту Response.text. Сначала проверяется наличие указания кодировки в HTTP-заголовках, а при его отсутствии используются библиотеки charset_normalizer или chardet.

Если установлена библиотека chardet, Requests предпочитает её, однако для Python3 она уже не является обязательной. При отсутствии chardet Requests использует charset-normalizer.

Requests не пытается определить кодировку, если в HTTP-заголовках нет явного указания charset и заголовок Content-Type содержит text. Согласно RFC 2616, в таком случае по умолчанию используется кодировка ISO-8859-1. Если требуется другая кодировка, вы можете вручную установить свойство Response.encoding или работать с необработанным содержимым через Response.content.

HTTP методы

Библиотека Requests предоставляет доступ к почти полному набору HTTP методов: GET, OPTIONS, HEAD, POST, PUT, PATCH и DELETE. Ниже приведены подробные примеры использования различных методов с применением GitHub API.

Начнем с самого распространённого метода — GET. HTTP GET является идемпотентным методом, который возвращает ресурс по заданному URL. Именно этот метод следует использовать для получения данных с веб-ресурса. Например, чтобы получить информацию о конкретном коммите (например, «a050faf») в репозитории Requests, можно сделать так:

>>> import requests
>>> r = requests.get('https://api.github.com/repos/psf/requests/git/commits/a050faf084662f3a352dd1a941f2c7c9f886d4ad')

Нужно убедиться, что GitHub ответил корректно. Если все в порядке, определим тип возвращаемого содержимого следующим образом:

>>> if r.status_code == requests.codes.ok:
...     print(r.headers['content-type'])
...
application/json; charset=utf-8

Таким образом, GitHub возвращает JSON. Это отлично, так как мы можем воспользоваться методом r.json(), чтобы преобразовать данные в объекты Python.

>>> commit_data = r.json()
>>> print(commit_data.keys())
['committer', 'author', 'url', 'tree', 'sha', 'parents', 'message']
>>> print(commit_data['committer'])
{'date': '2012-05-10T11:10:50-07:00', 'email': 'me@kennethreitz.com', 'name': 'Kenneth Reitz'}
>>> print(commit_data['message'])
makin' history

Пока всё просто. А теперь давайте немного поиграем с GitHub API. Конечно, можно изучить документацию, но интереснее будет использовать Requests. Например, можно воспользоваться методом OPTIONS, чтобы узнать, какие HTTP методы поддерживаются на используемом URL.

>>> verbs = requests.options(r.url)
>>> verbs.status_code
500

Что? Это не помогает! Оказывается, GitHub, как и многие другие API, фактически не реализует метод OPTIONS. Это досадная оплошность, но ничего — можно обратиться к документации. Если бы GitHub корректно реализовал OPTIONS, в заголовке должен был бы вернуться список разрешённых методов, например:

>>> verbs = requests.options('http://a-good-website.com/api/cats')
>>> print(verbs.headers['allow'])
GET,HEAD,POST,OPTIONS

Обратившись к документации, можно увидеть, что для коммитов единственный разрешённый метод — POST, который создаёт новый коммит. Поскольку мы используем git-репозиторий Requests, лучше не создавать лишние POST-запросы.

Вместо этого давайте поработаем с функцией Issues на GitHub.

Эта документация была добавлена в ответ на Issue #482. Поскольку такой issue уже существует, используем его в качестве примера. Начнем с его получения:

>>> r = requests.get('https://api.github.com/repos/psf/requests/issues/482')
>>> r.status_code
200

>>> issue = json.loads(r.text)

>>> print(issue['title'])
Feature any http verb in docs

>>> print(issue['comments'])
3

Отлично, issue содержит три комментария. Посмотрим на последний из них:

>>> r = requests.get(r.url + '/comments')
>>> r.status_code
200

>>> comments = r.json()

>>> print(comments[0].keys())
['body', 'url', 'created_at', 'updated_at', 'user', 'id']

>>> print(comments[2]['body'])
Probably in the "advanced" section

Кажется, это неудачное место для комментария. Давайте оставим сообщение, сообщив автору, что его предложение глупо. Но кто же автор?

>>> print(comments[2]['user']['login'])
kennethreitz

Итак, сообщим этому Кеннекту, что, по нашему мнению, данный пример стоит разместить в руководстве по быстрому старту. Согласно документации GitHub API, для этого нужно отправить POST-запрос в обсуждение. Попробуем так:

>>> body = json.dumps({u"body": u"Sounds great! I'll get right on it!"})
>>> url = u"https://api.github.com/repos/psf/requests/issues/482/comments"

>>> r = requests.post(url=url, data=body)
>>> r.status_code
404

Странно, 404? Видимо, требуется аутентификация. Кажется, это может быть сложно, но не волнуйтесь — Requests существенно упрощает работу с различными механизмами аутентификации, включая базовую (Basic Auth).

>>> from requests.auth import HTTPBasicAuth
>>> auth = HTTPBasicAuth('fake@example.com', 'not_a_real_password')

>>> r = requests.post(url=url, data=body, auth=auth)
>>> r.status_code
201

>>> content = r.json()
>>> print(content['body'])
Sounds great! I'll get right on it.

Отлично. Ой, нет! Я хотел добавить, что это займет некоторое время, так как мне нужно покормить кота. Если бы только я мог отредактировать этот комментарий! К счастью, GitHub позволяет использовать метод PATCH для изменения комментариев. Давайте сделаем это.

>>> print(content[u"id"])
5804413

>>> body = json.dumps({u"body": u"Sounds great! I'll get right on it once I feed my cat."})
>>> url = u"https://api.github.com/repos/psf/requests/issues/comments/5804413"

>>> r = requests.patch(url=url, data=body, auth=auth)
>>> r.status_code
200

Отлично. А теперь, чтобы немного подшутить над Кеннетом, я решил удалить этот комментарий, не сообщая ему, что я работаю над этим. GitHub позволяет удалять комментарии с помощью метода DELETE. Удаляем его:

>>> r = requests.delete(url=url, auth=auth)
>>> r.status_code
204
>>> r.headers['status']
'204 No Content'

Великолепно. Всё удалено. Последнее, что хочется узнать — сколько единиц лимита запросов я уже использовал. GitHub передает эту информацию в заголовках, поэтому вместо загрузки всей страницы отправим HEAD-запрос для получения только заголовков.

>>> r = requests.head(url=url, auth=auth)
>>> print(r.headers)
...
'x-ratelimit-remaining': '4995'
'x-ratelimit-limit': '5000'
...

Отлично. Пора написать программу на Python, которая будет использовать GitHub API во всех возможных интересных вариантах — ещё 4995 раз!

Пользовательские HTTP методы

Иногда вам может понадобиться работать с сервером, который поддерживает или требует использования HTTP методов, не описанных выше. Например, метод MKCOL, который используется некоторыми WEBDAV-серверами. Не переживайте — их можно использовать с Requests через встроенный метод requests.request. Например:

>>> r = requests.request('MKCOL', url, data=data)
>>> r.status_code
200  # При условии, что запрос выполнен корректно

Таким образом, вы можете использовать любой HTTP метод, поддерживаемый вашим сервером.

Адаптеры транспорта

Начиная с версии 1.0.0, Requests перешёл на модульную внутреннюю архитектуру. Одной из причин такого решения стало внедрение адаптеров транспорта. Адаптеры транспорта предоставляют механизм для определения способов взаимодействия с HTTP-сервисом, позволяя применять настройки, специфичные для каждого сервиса.

Requests поставляется с одним адаптером транспорта — HTTPAdapter <requests.adapters.HTTPAdapter>. Этот адаптер обеспечивает стандартное взаимодействие с HTTP и HTTPS посредством мощной библиотеки urllib3. При инициализации объекта Session <requests.Session> к нему автоматически прикрепляются два адаптера: для HTTP и для HTTPS.

Кроме того, Requests позволяет создавать собственные адаптеры транспорта для реализации дополнительных возможностей. После создания адаптер можно "примонтировать" к объекту сессии с указанием, к каким URL он должен применяться.

>>> s = requests.Session()
>>> s.mount('https://github.com/', MyAdapter())

Вызов метода mount регистрирует конкретный экземпляр адаптера для указанного префикса. После монтирования любой HTTP-запрос, URL которого начинается с заданного префикса, будет использовать данный адаптер.

Адаптер выбирается на основе наибольшего совпадения префикса. Учтите, что префиксы вроде http://localhost могут совпадать с http://localhost.other.com или http://localhost@other.com. Рекомендуется завершать полное имя хоста символом /.

Пример: Использование конкретной версии SSL

Команда Requests по умолчанию использует версию SSL, заданную в базовой библиотеке (urllib3). Обычно это не вызывает проблем, но иногда может потребоваться подключиться к сервису, использующему другую версию. Для этого можно создать собственный адаптер транспорта, основанный на HTTPAdapter, добавив параметр ssl_version, который передается в urllib3. Например, адаптер, принуждающий использование SSLv3:

import ssl
from urllib3.poolmanager import PoolManager

from requests.adapters import HTTPAdapter


class Ssl3HttpAdapter(HTTPAdapter):
    """"Адаптер транспорта, позволяющий использовать SSLv3.""""

    def init_poolmanager(self, connections, maxsize, block=False):
        self.poolmanager = PoolManager(
            num_pools=connections, maxsize=maxsize,
            block=block, ssl_version=ssl.PROTOCOL_SSLv3)

Пример: Автоматические повторные попытки

По умолчанию Requests не выполняет повторные попытки при неудачных соединениях. Однако можно реализовать автоматические повторы с множеством функций, включая экспоненциальное увеличение задержки, используя класс urllib3.util.Retry в рамках объекта Session:

from urllib3.util import Retry
from requests import Session
from requests.adapters import HTTPAdapter

s = Session()
retries = Retry(
    total=3,
    backoff_factor=0.1,
    status_forcelist=[502, 503, 504],
    allowed_methods={'POST'},
)
s.mount('https://', HTTPAdapter(max_retries=retries))

Блокирующий или неблокирующий режим?

При использовании стандартного адаптера транспорта Requests не поддерживает неблокирующий ввод-вывод. Атрибут Response.content блокирует выполнение до полной загрузки ответа. Если требуется более тонкое управление, возможности потоковой обработки позволяют получать данные частями, хотя такие вызовы тоже будут блокировать выполнение.

Если вас беспокоит блокирующий ввод-вывод, существуют проекты, интегрирующие Requests с асинхронными фреймворками Python, например: requests-threads, grequests, requests-futures и httpx.

Порядок заголовков

В редких случаях может потребоваться передать заголовки в определённом порядке. Если вы передадите в параметр headers объект типа OrderedDict, заголовки будут отсортированы согласно указанному порядку. Однако порядок заголовков по умолчанию имеет приоритет, поэтому если вы переопределяете стандартные заголовки через параметр headers, их порядок может отличаться от заданного.

Если это критично, рекомендуется установить заголовки по умолчанию на уровне объекта сессии, присвоив свойству Session.headers пользовательский OrderedDict, порядок которого будет всегда соблюдаться.

Таймауты

Для большинства запросов к внешним серверам важно задавать таймаут, чтобы избежать бесконечного ожидания в случае отсутствия ответа. По умолчанию таймауты не установлены, и ваш код может "зависнуть" на несколько минут или дольше.

Connect timeout — время в секундах, которое Requests ждет установления соединения с сервером (аналогично вызову connect() на сокете). Рекомендуется задавать этот таймаут немного больше, чем стандартное окно повторной передачи TCP-пакетов (см. TCP packet retransmission window).

После установления соединения и отправки запроса, read timeout — это время, которое клиент будет ждать между поступлением байтов от сервера (обычно до получения первого байта).

Если вы укажете одно число, например:

r = requests.get('https://github.com', timeout=5)

то это значение будет применяться и к connect, и к read таймаутам. Чтобы задать их отдельно, передайте таймаут в виде кортежа:

r = requests.get('https://github.com', timeout=(3.05, 27))

Если сервер очень медленный, можно установить таймаут в None, чтобы ждать ответа бесконечно (но, возможно, за это время вы успеете выпить чашку кофе):

r = requests.get('https://github.com', timeout=None)
Connect таймаут применяется к каждой попытке подключения к IP-адресу. Если для домена существует несколько адресов, urllib3 будет пробовать их последовательно, что может привести к суммарному таймауту, в несколько раз превышающему указанное время (например, если сервер имеет и IPv4, и IPv6 адреса, время ожидания может удвоиться).
Ни connect, ни read таймауты не основаны на реальном времени (wall clock). Это означает, что фактическое время выполнения запроса может превышать указанное значение.