Добавление функции обратного вызова при каждой повторной попытке с использованием запросов/urllib3

Я реализовал механизм повтора для сеанса requests, используя urllib3.util.retry как предложено как здесь и здесь.

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

Чтобы пояснить еще больше, если бы либо объект Retry, либо метод запросов get имели возможность добавить функцию обратного вызова, это было бы здорово. Может быть, что-то вроде:

import requests
from requests.packages.urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter

def retry_callback(url):
    print url   

s = requests.Session()
retries = Retry(total=5, status_forcelist=[ 500, 502, 503, 504 ])
s.mount('http://', HTTPAdapter(max_retries=retries))

url = 'http://httpstat.us/500'
s.get(url, callback=retry_callback, callback_params=[url])

Я знаю, что для печати URL-адреса я могу использовать ведение журнала, но это всего лишь простой пример для более сложного использования.


person A. Sarid    schedule 05.07.2018    source источник


Ответы (1)


Вы можете создать подкласс класса Retry, чтобы добавить эту функциональность.

Это полный процесс взаимодействия с экземпляром Retry для данной попытки подключения:

  • Retry.increment() is called with the current method, url, response object (if there is one), and exception (if one was raised) whenever an exception is raised, or a 30x redirection response was returned, or the Retry.is_retry() method returns true.
    • .increment() will re-raise the error (if there was one) and the object was configured not to retry that specific class of errors.
    • .increment() вызывает Retry.new() для создания обновленного экземпляра с обновленными всеми релевантными счетчиками и измененным атрибутом history новым RequestHistory() instance (именованный кортеж).
    • .increment() вызовет исключение MaxRetryError, если Retry.is_exhausted(), вызванное возвращаемым значением Retry.new(), истинно. is_exhausted() возвращает значение true, когда значение любого из отслеживаемых им счетчиков падает ниже 0 (счетчики, установленные на None, игнорируются).
    • .increment() возвращает новый экземпляр Retry.
  • возвращаемое значение Retry.increment() заменяет старый отслеживаемый экземпляр Retry. Если было перенаправление, то вызывается Retry.sleep_for_retry() (засыпает, если есть заголовок Retry-After), в противном случае вызывается Retry.sleep() (который вызывает self.sleep_for_retry() для обработки заголовка Retry-After, в противном случае просто засыпает, если есть политика отсрочки). Затем выполняется рекурсивный вызов соединения с новым экземпляром Retry.

Это дает вам 3 хороших точки обратного вызова; в начале .increment(), при создании нового экземпляра Retry и в диспетчере контекста около super().increment(), чтобы позволить обратному вызову наложить вето на исключение или обновить возвращенную политику повторных попыток при выходе.

Вот как будет выглядеть хук в начале .increment():

import logging

logger = getLogger(__name__)

class CallbackRetry(Retry):
    def __init__(self, *args, **kwargs):
        self._callback = kwargs.pop('callback', None)
        super(CallbackRetry, self).__init__(*args, **kwargs)
    def new(self, **kw):
        # pass along the subclass additional information when creating
        # a new instance.
        kw['callback'] = self._callback
        return super(CallbackRetry, self).new(**kw)
    def increment(self, method, url, *args, **kwargs):
        if self._callback:
            try:
                self._callback(url)
            except Exception:
                logger.exception('Callback raised an exception, ignoring')
        return super(CallbackRetry, self).increment(method, url, *args, **kwargs)

Обратите внимание, что аргумент url на самом деле представляет собой только URL-путь, часть запроса о сетевом расположении опущена (вам придется извлечь ее из аргумента _pool, у него есть атрибуты .scheme, .host и .port). .

Демо:

>>> def retry_callback(url):
...     print('Callback invoked with', url)
...
>>> s = requests.Session()
>>> retries = CallbackRetry(total=5, status_forcelist=[500, 502, 503, 504], callback=retry_callback)
>>> s.mount('http://', HTTPAdapter(max_retries=retries))
>>> s.get('http://httpstat.us/500')
Callback invoked with /500
Callback invoked with /500
Callback invoked with /500
Callback invoked with /500
Callback invoked with /500
Callback invoked with /500
Traceback (most recent call last):
  File "/.../lib/python3.6/site-packages/requests/adapters.py", line 440, in send
    timeout=timeout
  File "/.../lib/python3.6/site-packages/urllib3/connectionpool.py", line 732, in urlopen
    body_pos=body_pos, **response_kw)
  File "/.../lib/python3.6/site-packages/urllib3/connectionpool.py", line 732, in urlopen
    body_pos=body_pos, **response_kw)
  File "/.../lib/python3.6/site-packages/urllib3/connectionpool.py", line 732, in urlopen
    body_pos=body_pos, **response_kw)
  [Previous line repeated 1 more times]
  File "/.../lib/python3.6/site-packages/urllib3/connectionpool.py", line 712, in urlopen
    retries = retries.increment(method, url, response=response, _pool=self)
  File "<stdin>", line 8, in increment
  File "/.../lib/python3.6/site-packages/urllib3/util/retry.py", line 388, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='httpstat.us', port=80): Max retries exceeded with url: /500 (Caused by ResponseError('too many 500 error responses',))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../lib/python3.6/site-packages/requests/sessions.py", line 521, in get
    return self.request('GET', url, **kwargs)
  File "/.../lib/python3.6/site-packages/requests/sessions.py", line 508, in request
    resp = self.send(prep, **send_kwargs)
  File "/.../lib/python3.6/site-packages/requests/sessions.py", line 618, in send
    r = adapter.send(request, **kwargs)
  File "/.../lib/python3.6/site-packages/requests/adapters.py", line 499, in send
    raise RetryError(e, request=request)
requests.exceptions.RetryError: HTTPConnectionPool(host='httpstat.us', port=80): Max retries exceeded with url: /500 (Caused by ResponseError('too many 500 error responses',))

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

person Martijn Pieters    schedule 09.07.2018
comment
Вот это да. Спасибо за этот подробный ответ. Поэтому, если я хочу добавить также аргумент callback_params, я могу сделать это так же, как вы сделали с callback, и просто передать их, когда я вызываю саму функцию обратного вызова, верно? - person A. Sarid; 11.07.2018
comment
@A.Sarid: да, вы можете добавить любое количество дополнительных атрибутов в свой подкласс и использовать их по своему усмотрению. Обновите метод new(), чтобы скопировать любые такие атрибуты в словарь kw перед вызовом метода super().new() для создания копии. - person Martijn Pieters; 11.07.2018
comment
@A.Sarid: Лично я не добавил бы такую ​​функцию params. Это усложняет обработку обратного вызова в классе и не требуется. Вместо этого передайте функцию обратного вызова, которая может обрабатывать набор аргументов по умолчанию. - person Martijn Pieters; 11.07.2018
comment
Спасибо! Да, набор аргументов по умолчанию — это хорошо, но если я хочу указать эти аргументы при создании своего класса CallbackRetry, что будет для меня лучшим вариантом? - person A. Sarid; 12.07.2018
comment
@A.Sarid: вам нужно создать список аргументов и передать его обратному вызову с помощью *args: args = [getattr(self, argname) for argname in self.callback_params] и self.callback(*args). Я настоятельно рекомендую вам не делать этого. Вместо этого используйте обратный вызов оболочки, оболочка принимает все аргументы, а затем вызывает ваш фактический обратный вызов только с url или подобным: callback = lambda method, url, *args, **kwargs: real_callback(url) (в сценарии, где url является вторым аргументом, который будет передан подклассу Retry). - person Martijn Pieters; 12.07.2018