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

В этом документе рассматриваются некоторые из дополнительных функций Requests.

Объекты Session

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

У объекта Session есть все методы основного API Requests.

Давайте сохраним некоторые cookie в запросах:

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"}}'

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

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'})

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

Однако обратите внимание, что параметры уровня метода не будут сохраняться в requests, даже если используется сеанс. В этом примере cookie будут отправляться только с первым запросом, но не со вторым:

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": {}}'

Если вы хотите вручную добавить cookie в сеанс, используйте Служебные cookie функции для управления Session.cookies.

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

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

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

Удалить значение из параметра Dict

Иногда вам нужно пропустить ключи уровня сеанса из параметра dict. Для этого вы просто устанавливаете значение этого ключа на None в параметре уровня метода. Он будет автоматически пропущен.

Все значения, содержащиеся в сеансе, доступны вам напрямую. См. Документы по Session API, чтобы узнать больше.

Объекты запроса и ответа

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

>>> 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'}

Однако, если мы хотим получить заголовки, которые были отправлены серверу, мы просто обращаемся к запросу, а затем к заголовкам запроса:

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

Подготовка Requests

Каждый раз, когда вы получаете объект Response из вызова API или сеанса, атрибут request фактически является использованным PreparedRequest. В некоторых случаях вы можете захотеть проделать дополнительную работу с телом или заголовками (или чем-то ещё) перед отправкой запроса. Простой рецепт для этого следующий:

from requests import Request, Session

s = Session()

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

# сделать что-нибудь с prepped.body
prepped.body = 'No, I want exactly this as the body.'

# сделать что-нибудь с prepped.headers
del prepped.headers['Content-Type']

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

print(resp.status_code)

Поскольку вы не делаете ничего особенного с объектом Request, вы готовите его немедленно и изменяете объект PreparedRequest. Затем вы отправляете его с другими параметрами, которые вы бы отправили на requests.* или Session.*.

Однако приведенный выше код теряет некоторые преимущества наличия объекта Requests Session. В частности, к вашему запросу не будет применяться состояние уровня Session, такое как cookie. Чтобы получить 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.body = 'Seriously, send exactly these bytes.'

# сделать что-нибудь с prepped.headers
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 запросов, как веб-браузер. По умолчанию проверка SSL включена, и Requests выдаст ошибку 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. Если REQUESTS_CA_BUNDLE не установлен, CURL_CA_BUNDLE будет использоваться как резервный.

Requests также может игнорировать проверку сертификата SSL, если для verify задано ложное значение:

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

Обратите внимание, что если для verify установлено значение False, requests примет любой сертификат TLS, представленный сервером, и будет игнорировать несоответствия имени хоста и/или сертификаты с истекшим сроком действия, что сделает ваше приложение уязвимым для атак типа «человек посередине» (MitM). Установка параметра verify на False может быть полезна во время локальной разработки или тестирования.

По умолчанию для verify установлено значение истина. Опция verify применяется только к сертификатам хоста.

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

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

>>> 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. Сертификаты обновлялись только один раз для каждой версии Requests. Когда certifi не был установлен, это приводило к чрезвычайно устаревшим пакетам сертификатов при использовании значительно более старых версий Requests.

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

Рабочий процесс с содержимым body

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

tarball_url = 'https://github.com/psf/requests/tarball/master'
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 urllib3.HTTPResponse по адресу Response.raw.

Если установить stream на True при выполнении запроса, Requests не сможет освободить соединение обратно с пулом, если вы не потребляете все данные или не вызовете Response.close. Это может привести к неэффективности подключений. Если вы обнаружите, что частично читаете тело запросов (или не читаете их вообще) при использовании stream=True, вам следует сделать запрос в операторе with, чтобы он всегда был закрыт:

with requests.get('https://httpbin.org/get', stream=True) as r:
    # Сделать что-нибудь с ответом.

Keep-Alive

Отличные новости — благодаря urllib3, keep-alive 100% автоматический в течение сеанса! Любой из запросов, созданный вами в течение сеанса, автоматически повторно использует соответствующее соединение!

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

Потоковая загрузка

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

with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

Предупреждение

Настоятельно рекомендуется открывать файлы в двоичном режиме. Это связано с тем, что Requests может попытаться заполнить заголовок Content-Length, и если это произойдет, это значение будет установлено на количество байтов в файле. Ошибки могут возникнуть, если вы откроете файл в текстовом режиме.

Фрагментированное кодирование Requests

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

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 любое целое число.

POST несколько файлов с многократным кодированием

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

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

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

>>> 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, и если это произойдёт, это значение будет установлено на количество байтов в файле. Могут возникнуть ошибки, если вы откроете файл в текстовом режиме.

Хуки событий

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

В наличии хуки:

response:

Ответ, созданный на запрос.

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

hooks={'response': print_url}

callback_function получит фрагмент данных в качестве своего первого аргумента.

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

Если при выполнении обратного вызова возникает ошибка, выдается предупреждение.

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

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

Давайте напечатаем некоторые аргументы метода requests во время выполнения:

>>> 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

Вы также можете добавить хуки к экземпляру Session. Любые добавленные хуки будут вызываться при каждом запросе к сеансу. Например:

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

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

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

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

У любого вызываемого объекта, который передаётся как аргумент auth методу запроса, будет возможность изменить запрос перед его отправкой.

Реализации аутентификации являются подклассами AuthBase, и их легко определить. Requests предоставляет две общие реализации схемы аутентификации в requests.auth: HTTPBasicAuth и 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 Auth:

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

Стриминг Requests

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

import json
import requests

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

for line in r.iter_lines():

    # отфильтровать оставшиеся новые строки
    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))

Предупреждение

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

lines = r.iter_lines()
# Сохранить первую строку на потом или просто пропустить

first_line = next(lines)

for line in lines:
    print(line)

Прокси

Если вам нужно использовать прокси, вы можете настроить отдельный requests с аргументом 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:

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')

Когда конфигурация прокси-серверов не переопределяется в python, как показано выше, по умолчанию Requests полагается на конфигурацию прокси, определенную стандартными переменными среды http_proxy, https_proxy, no_proxy и curl_ca_bundle. Также поддерживаются варианты этих переменных в верхнем регистре. Поэтому вы можете настроить их для конфигурации Requests (установить только те, которые соответствуют вашим потребностям):

$ export HTTP_PROXY="http://10.10.1.10:3128"
$ export HTTPS_PROXY="http://10.10.1.10:1080"

$ 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)

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

$ export curl_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

Добавлено в версии 2.10.0.

Помимо основных 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 происходит на клиенте, а не на прокси-сервере. Это соответствует curl, который использует схему, чтобы решить, выполнять ли разрешение DNS на клиенте или прокси. Если вы хотите разрешать домены на прокси-сервере, используйте socks5h в качестве схемы.

Согласие

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

Кодировки

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

Если chardet установлен, requests использует его, однако для python3 chardet больше не является обязательной зависимостью. Библиотека chardet является зависимостью с лицензией LGPL, и некоторые пользователи requests не могут зависеть от обязательных зависимостей с лицензией LGPL.

Если вы устанавливаете request без указания [use_chardet_on_py3]] extra, а chardet ещё не установлен, requests использует charset- normalizer (с лицензией MIT), чтобы угадать кодировку. Для Python 2 requests использует только chardet и является там обязательной зависимостью.

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

HTTP-глаголы

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

Мы начнём с наиболее часто используемого глагола: GET. HTTP GET — это идемпотентный метод, который возвращает ресурс по заданному URL-адресу. В результате это глагол, который вы должны использовать при попытке получить данные из веб-сайта. Пример использования — попытка получить информацию о конкретном коммите из GitHub. Предположим, мы хотим получить коммит 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. Мы можем воспользоваться командой 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, который создаёт новый коммит. Поскольку мы используем репо Requests, нам, вероятно, следует избегать неуклюжих POST’ов для него. Вместо этого давайте поиграем с функцией Issues на GitHub.

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

>>> 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

Круто, у нас есть три комментария. Давайте посмотрим на последний из них.

>>> 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

Хорошо, давайте скажем этому парню Кеннету, что, по нашему мнению, этот пример должен быть включен в руководство по быстрому старту. Согласно документу API GitHub, это можно сделать с помощью 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

Ха, это странно. Вероятно, нам нужно пройти аутентификацию. Это будет больно, правда? Неправда. Requests упрощает использование многих форм аутентификации, включая очень распространенную базовую аутентификацию.

>>> 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 позволяет нам использовать другой HTTP-глагол, 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, которая злоупотребляет API GitHub всеми интересными способами, ещё 4995 раз.

Пользовательские глаголы

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

>>> r = requests.request('MKCOL', url, data=data)
>>> r.status_code
200 # Предполагая, что ваш вызов был правильным

Используя это, вы можете использовать любую команду метода, которую позволяет ваш сервер.

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

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

Requests поставляется с одним транспортным адаптером HTTPAdapter. Этот адаптер обеспечивает взаимодействие Requests по умолчанию с HTTP и HTTPS с использованием мощной библиотеки urllib3. При инициализации Requests Session один из них присоединяется к объекту Session для HTTP, а другой — для HTTPS.

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

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

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

Многие детали реализации транспортного адаптера выходят за рамки этой документации, но взгляните на следующий пример для простого варианта использования SSL. Более того, вы можете создать подкласс BaseAdapter.

Пример: конкретная версия 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 не обеспечивает никакого неблокирующего ввода-вывода. Свойство Response.content будет заблокировано до тех пор, пока не будет загружен весь ответ. Если вам требуется большая детализация, функции потоковой передачи библиотеки (см. Requests cтриминга) позволяют получать меньшие количества ответа за раз. Однако эти вызовы всё равно будут блокироваться.

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

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

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

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

Таймауты

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

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

После того, как ваш клиент подключился к серверу и отправил HTTP-запрос, таймаут чтения — это количество секунд, в течение которых клиент будет ждать, пока сервер отправит ответ. (В частности, это количество секунд, в течение которых клиент будет ждать паузы между байтами, отправленными с сервера. В 99,9% случаев это время до того, как сервер отправит первый байт).

Пример указания одного значения для тайм-аута:

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

Значение тайм-аута будет применяться к тайм-аутам подключения и чтения. Укажите кортеж, если вы хотите установить значения отдельно:

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

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

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