суббота, 19 декабря 2015 г.

Перевод aiohttp http client

Перевод официальной документации aiohttp http client

Для Python3.5+

HTTP клиент

Переводил почти дословно. Перевод так себе, но так как информации на русском о aiohttp почти нету, думаю, будет полезно.

Создание запроса (Request)

Импортируем aiohttp модуль:
import aiohttp
Теперь давайте попробуем получить веб-страницу. Например получим публичную хронику гитхаба
r = await aiohttp.get('https://api.github.com/events')
Теперь мы имеем объект ClientResponse с именем r. Мы можем получить всю информацию с этого объекта. Обязательный параметр aiohttp.get это URL страницы. Для того чтобы сделать POST запрос нужно использовать aiohttp.post:
r = await aiohttp.post('http://httpbin.org/post', data=b'data')
Другие HTTP методы:
r = await aiohttp.put('http://httpbin.org/put', data=b'data')
r = await aiohttp.delete('http://httpbin.org/delete')
r = await aiohttp.head('http://httpbin.org/get')
r = await aiohttp.options('http://httpbin.org/get')
r = await aiohttp.patch('http://httpbin.org/patch', data=b'data')

Передача параметров в УРЛах (URLs)

Часто приходится передавать данные в строке запроса URL (query string). Эти данные будут приведены в виде пар ключ=значение после знака вопроса [?], например, httpbin.org/get?key=val. aiohttp позволяет передавать эти аргументы с словаря, используя именованный аргумент params. Например, если Вы хотите передать key1=value1 и key2=value2 (отправить запрос на урл http://httpbin.org/get?key1=value1&key2=value2) можно использовать следующий код:
>>> import asyncio
>>> import aiohttp
>>> async def test():
...     payload = {'key1': 'value1', 'key2': 'value2'}
...     async with aiohttp.get('http://httpbin.org/get', params=payload) as r:
...         print(r.url)
... 
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(test())
http://httpbin.org/get?key2=value2&key1=value1
>>> loop.close() 
Примечание
В официальной документации дан пример:
payload = [('key', 'value1'), ('key', 'value2')]
async with aiohttp.get('http://httpbin.org/get',
                       params=payload) as r:
    assert r.url == 'http://httpbin.org/get?key=value2&key=value1'
который неправильный, так как словарь отдает элементы не обязательно в таком порядке как они записаны в словаре, то есть вполне может быть такая ситуация:
>>> import asyncio
>>> import aiohttp
>>> async def test():
...     payload = {'key1': 'value1', 'key2': 'value2'}
...     async with aiohttp.get('http://httpbin.org/get', params=payload) as r:
...         print(r.url)
...         print(r.url == 'http://httpbin.org/get?key2=value2&key1=value1')
... 
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(test())
http://httpbin.org/get?key1=value1&key2=value2
False
>>> loop.close()

Вы можете увидеть что УРЛ правильно сформирован. При этом данные будут закодированы. Также можно передать список двухэлементных кортежей, при такой передачи можно указать несколько значений для одного ключа:
...
>>> async def test():
...     payload = [('key', 'value1'), ('key', 'value2')]
...     async with aiohttp.get('http://httpbin.org/get', params=payload) as r:
...         print(r.url)
... 
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(test())
http://httpbin.org/get?key=value1&key=value2
…
Вы также можете передавать в параметр param строку, но данные не будут закодированы:
...
>>> async def test():
...     payload = {'key': 'value+1'}
...     async with aiohttp.get('http://httpbin.org/get', params=payload) as r:
...         print(r.url)
...     async with aiohttp.get('http://httpbin.org/get', params='key=value+1') as r:
...         print(r.url)
... 
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(test())
http://httpbin.org/get?key=value%2B1
http://httpbin.org/get?key=value+1
...

Содержимое ответа

Мы можем читать содержимое ответа. Рассмотрим хронику гитхаба:
r = await aiohttp.get('https://api.github.com/events')
print(await r.text())
и увидим что-то вроде:
'[{"created_at":"2015-06-12T14:06:22Z","public":true,"actor":{…
aiohttp автоматически декодирует данные полученные с сервера. Вы можете принудительно указать кодировку:
await r.text(encoding='windows-1251')

Двоичное содержимое ответа

Вы также можете получить ответ в байт-строке для нетекстовых запросов:
print(await r.read())
b'[{"created_at":"2015-06-12T14:06:22Z","public":true,"actor":{…
gzip и deflate компрессоры автоматически декодирует.

Ответ в формате JSON

Если Вы имеете дело с JSON данными, то также есть встроенный JSON декодер:
async with aiohttp.get('https://api.github.com/events') as r:
    print(await r.json())
В случае ошибки декодирования будет вызвано исключение. В метод также можно передавать кодировку и пользовательскую функцию для обработки JSON.

Потоковый ответ

Хоть методы read(), json() и text() удобны в использовании, но Вы должны использовать их осторожно, так как эти методы загружают все данные в память. Например, если Вы хотите загрузить файл размером в несколько гигабайт, то перед тем как с ним что-то делать эти методы сначала загрузят этот файл в оперативную память. Чтобы решить эту проблему Вы можете использовать атрибут content, он является экземпляром касса aiohttp.StreamReader. gzip и deflate автоматически декодируют данные:
async with aiohttp.get('https://api.github.com/events') as r:
    await r.content.read(10)
Для получения файла Вы можете использовать следующий паттерн:
with open(filename, 'wb') as fd:
    while True:
        chunk = await r.content.read(chunk_size)
        if not chunk:
            break
        fd.write(chunk)
После чтения с content Вы уже не можете использовать read(), json() и text()

Освобождение канала после получения ответа

Не забудьте освободить канал после получение ответа. Это будет гарантировать явное поведение и правильное пул соединений. Самый простой способ это использование async with:
async with client.get(url) as resp:
    pass
Но также можно явно вызвать метод release():
await r.release()
Этого можно не делать если Вы используете read(), json() или text(), так как они это делает автоматически, но все же лучше перестраховываться.

Пользовательские заголовки (headers)

Если Вам нужно добавить свой HTTP заголовок в запрос, Вы можете передать словарь в именованный параметр headers. Например, Вы хотите указать тип содержимого:
import json
url = 'https://api.github.com/some/endpoint'
payload = {'some': 'data'}
headers = {'content-type': 'application/json'}
await aiohttp.post(url, data=json.dumps(payload), headers=headers) 

Пользовательские куки (cookies)

Для отправки своих куков при запросе, можно передать словарь в именованный параметр cookies. Например:
url = 'http://httpbin.org/cookies'
cookies = dict(cookies_are='working')

async with aiohttp.get(url, cookies=cookies) as r:
    assert await r.json() == {"cookies": {"cookies_are": "working"}}

Более сложные POST запросы

Возможно, вам требуется отправлять более сложные запросы, например — данные форм, как формы в HTML. Что бы сделать это — просто передайте словарь с данными именованному параметру data:
payload = {'key1': 'value1', 'key2': 'value2'}
async with aiohttp.post('http://httpbin.org/post',
                        data=payload) as r:
    print(await r.text())
{
  ...
  "form": {
    "key2": "value2",
    "key1": "value1"
  },
  ...
}
Есди нужно передать данные не в виде формы, для этого нужно передать строку вместо словаря и данные будут добавлены напрямую. Например, GitHub API v3 принимает POST/PATCH в формате JSON:
import json
url = 'https://api.github.com/some/endpoint'
payload = {'some': 'data'}

r = await aiohttp.post(url, data=json.dumps(payload))

POST для файлов составной кодировки

Пример загрузки Multipart-encoded файлов:
url = 'http://httpbin.org/post'
files = {'file': open('report.xls', 'rb')}

await aiohttp.post(url, data=files)
Вы так же можете указать имя файла и тип контента явным образом:
url = 'http://httpbin.org/post'
data = FormData()
data.add_field('file',
               open('report.xls', 'rb'),
               filename='report.xls',
               content_type='application/vnd.ms-excel')

await aiohttp.post(url, data=data)
Если вы передаете объект файла в качестве параметра data, aiohttp будет автоматически отправлять его на сервер потоком. Проверяйте StreamReader на поддержку формата информации. Потоковая загрузка файлов aiohttp поддерживает несколько способов потоковой загрузки, которые позволяют отправлять большие файлы не загружая их целиком в память. Простой способ это передать файловый объект в теле POST ответа:
with open('massive-body', 'rb') as f:
   await aiohttp.post('http://some.url/streamed', data=f)
Или вы можете использовать coroutine, которая будет отдавать байт-объекты
@asyncio.coroutine
def my_coroutine():
   chunk = yield from read_some_data_from_somewhere()
   if not chunk:
      return
   yield chunk
Предупреждение
yield запрещен внутри async def .
Примечание
Это не стандартная coroutine, поэтому здесь не можно использовать yield from my_coroutine(). aiohttp обрабатывает их внутри.

Также можно использовать StreamReader объект. Припустим мы хотим загрузить файл в другом запросе и вычислить хэш файла:
async def feed_stream(resp, stream):
    h = hashlib.sha256()

    while True:
        chunk = await resp.content.readany()
        if not chunk:
            break
        h.update(chunk)
        s.feed_data(chunk)

    return h.hexdigest()

resp = aiohttp.get('http://httpbin.org/post')
stream = StreamReader()
loop.create_task(aiohttp.post('http://httpbin.org/post', data=stream))

file_hash = await feed_stream(resp, stream)
Так как атрибут содержимого ответа является StreamReader, Вы можете объеденить get и post запросы вместе (aka HTTP pipelining):
r = await aiohttp.request('get', 'http://python.org')
await aiohttp.post('http://httpbin.org/post',
                   data=r.content)

Загрузка предварительно сжатых данных

Чтобы загрузить данные, которые уже сжатые, вызовите функцию запроса с compress=False, а в заголовке значение Content-Encoding должно указывать алгоритм сжатия (обычно это deflate или zlib):
@asyncio.coroutine
def my_coroutine( my_data):
    data = zlib.compress(my_data)
    headers = {'Content-Encoding': 'deflate'}
    yield from aiohttp.post(
        'http://httpbin.org/post', data=data, headers=headers,
        compress=False)

Keep-Alive, пулы соединений и совместное использоваение куки

Чтобы использовать куки в нескольких запросах нужно создать объект ClientSession:
session = aiohttp.ClientSession()
await session.post(
     'http://httpbin.org/cookies/set/my_cookie/my_value')
# получили куки и они сохранились в session
# и при следующем использовании session будут переданы ранее сохраненный куки
#
# читаем куки
async with session.get('http://httpbin.org/cookies') as r:
    json = await r.json()
    assert json['cookies']['my_cookie'] == 'my_value'
Вы можете установить значения заголовков куки по умолчанию, которые будут использоваться во всех запросах через session:
session = aiohttp.ClientSession(
    headers={"Authorization": "Basic bG9naW46cGFzcw=="})
async with s.get("http://httpbin.org/headers") as r:
    json = yield from r.json()
    assert json['headers']['Authorization'] == 'Basic bG9naW46cGFzcw=='
По-умолчанию aiohttp не использует пул соединения, то есть каждый вызов request() будет создавать новое соединение. Для того чтобы использовать пул, то есть не открывать каждый раз новое соединение, можно использовать ClientSession, объекты которого будут сами создавать пулы.

Коннекторы

Для настройки или изменения запросов на транспортном уровне Вы можете передать свой коннектор в aiohttp.request() и «семью» (get, post и т. д.). Например:
conn = aiohttp.TCPConnector()
r = await aiohttp.get('http://python.org', connector=conn)
В ClientSession также можно передавать свой коннектор:
session = aiohttp.ClientSession(connector=aiohttp.TCPConnector())

Ограничение количества соединений в пуле

Чтобы ограничить количество одновременных подключений к одной и той же конечной точки (уникальность конечной точки определяется тремя параметрами: host, port, is_ssl) Вы можете установить именованный параметр limit в коннекторе:
conn = aiohttp.TCPConnector(limit=30)
В этом примере максимальное количество одновременных соединений равно 30.

SSL проверка в TCP сокетах

Конструктор aiohttp.connector.TCPConnector принимает два взаимоисключающих параметра: verify_ssl и ssl_context. По умолчанию для протокола HTTPS коннектор проверяет ssl сертификат. Для отключения проверки нужно установить verify_ssl = False:
conn = aiohttp.TCPConnector(verify_ssl=False)
session = aiohttp.ClientSession(connector=conn)
r = await session.get('https://example.com')
Если Вы хотите использовать свои параметры SSL, например, использовать собственные файлы сертификации, то Вы можете создать экземпляр ssl.SSLContext и передать его в коннектор с помощью именованного параметра ssl_context:
sslcontext = ssl.create_default_context(cafile='/path/to/ca-bundle.crt')
conn = aiohttp.TCPConnector(ssl_context=sslcontext)
session = aiohttp.ClientSession(connector=conn)
r = await session.get('https://example.com')
Также можно проверять сертификаты через MD5, SHA1 или SHA256 fingerprint:
# Попытка подключится к https://www.python.org
# с фальшивым сертификатом
bad_md5 = b'\xa2\x06G\xad\xaa\xf5\xd8\\J\x99^by;\x06='
conn = aiohttp.TCPConnector(fingerprint=bad_md5)
session = aiohttp.ClientSession(connector=conn)
exc = None
try:
    r = yield from session.get('https://www.python.org')
except FingerprintMismatch as e:
    exc = e
assert exc is not None
assert exc.expected == bad_md5

# www.python.org cert's actual md5
assert exc.got == b'\xca;I\x9cuv\x8es\x138N$?\x15\xca\xcb'
Примечание. Этот пример с официальной документации, но при запуске у Вас будет вызвано исключение, так как не учли что сертификат может поменяться и сейчас:
. . .
...     print(exc.got)
. . .
b'\xe6\x85.78\xdf\xf3\xf2\x81gV\xf7\xf6$\xd8\xf3'
Сертификат передается в формате DER и если у Вас он в PEM, то Вам нужно конвертировать его в DER, например так:
openssl x509 -in crt.pem -inform PEM -outform DER > crt.der.
Совет: для конвертирования с шестнадцатеричного значения в двоичную байт-строку Вы можете использовать binascii.unhexlify:
>>> from binascii import unhexlify
>>> md5_hex = 'ca3b499c75768e7313384e243f15cacb'
>>> print(unhexlify(md5_hex) == b'\xca;I\x9cuv\x8es\x138N$?\x15\xca\xcb')
True

Сокет домена UNIX (Unix domain sockets)

Если Ваш HTTP сервер использует сокет домена UNIX вы можете использовать aiohttp.connector.UnixConnector:
conn = aiohttp.UnixConnector(path='/path/to/socket')
r = await aiohttp.get('http://python.org', connector=conn)

Поддержка прокси

Для использования прокси Вы должны использовать aiohttp.connector.ProxyConnector:
conn = aiohttp.ProxyConnector(proxy="http://some.proxy.com")
r = await aiohttp.get('http://python.org',
                      connector=conn)
ProxyConnector также поддерживает прокси-авторизацию:
conn = aiohttp.ProxyConnector(
   proxy="http://some.proxy.com",
   proxy_auth=aiohttp.BasicAuth('user', 'pass'))
session = aiohttp.ClientSession(connector=conn)
async with session.get('http://python.org') as r:
    assert r.status == 200
Учетные данные могут быть переданы в урле:
conn = aiohttp.ProxyConnector(
    proxy="http://user:pass@some.proxy.com")
session = aiohttp.ClientSession(connector=conn)
async with session.get('http://python.org') as r:
    assert r.status == 200

Коды состояния ответов

Вы можете проверить код состояния ответа:
async with aiohttp.get('http://httpbin.org/get') as r:
    assert r.status == 200

Заголовки ответа

Вы можете посмотреть заголовки ответа сервера:
<_cimultidictproxy data-blogger-escaped-17:11:49="" data-blogger-escaped-19="" data-blogger-escaped-2015="" data-blogger-escaped-application="" data-blogger-escaped-at="" data-blogger-escaped-dec="" data-blogger-escaped-gmt="" data-blogger-escaped-json="" data-blogger-escaped-keep-alive="" data-blogger-escaped-nginx="" data-blogger-escaped-true="">
По RFC 7230, имена заголовков HTTP нечувствительны к регистру, поэтому Вы можете получить значение не в зависимости от регистра ключа:
>>> r.headers['Content-Type']
'application/json'
>>> r.headers.get('content-type')
'application/json'

Куки ответа

Если ответ содержит некоторые куки, Вы можете получить быстрый доступ к ним:
url = 'http://example.com/some/cookie/setting/url'
async with aiohttp.get(url) as r:
    print(r.cookies['example_cookie_name'])
Примечание: Для того чтобы собрать все куки со всей цепочки перенаправлений нужно использовать aiohttp.ClientSession, иначе ответ будет содержать только куки с последнего элемента в цепочки перенаправлений. История ответа Если запрос был перенаправлен, Вы можете посмотреть историю ответов используя атрибут history в виде кортежа. Если перенаправлений не было или allow_redirects=False, то будет возвращена пустаой кортеж.

Таймауты

Вы должны использовать asyncio.wait_for() если Вы хотите ограничить время ожидание ответа:
>>> asyncio.wait_for(aiohttp.get('http://github.com'),
...                             0.001)
Traceback (most recent call last)\:
  File "", line 1, in 
asyncio.TimeoutError()
Или обернуть вызов клиента в контекстный менеджер Timeout:
with aiohttp.Timeout(0.001):
    async with aiohttp.get('https://github.com') as r:
        await r.text()
Примечание
Таймаут устанавливает ограничение не на загрузку ответа, а на ожидание получения первого бита информации.

1 комментарий: