Posso definir max_retries para orders.request?

182

O módulo de pedidos do Python é simples e elegante, mas uma coisa me incomoda. É possível obter um request.exception.ConnectionError com uma mensagem como:

Max retries exceeded with url: ...

Isso implica que as solicitações podem tentar acessar os dados várias vezes. Mas não há uma única menção dessa possibilidade em nenhum lugar dos documentos. Olhando para o código fonte, não encontrei nenhum lugar onde pudesse alterar o valor padrão (presumivelmente 0).

Portanto, é possível definir de alguma forma o número máximo de novas tentativas para solicitações?

Kirill Zaitsev
fonte
9
Alguma atualização sobre isso com solicitações no 2.x? Adoraria uma implementação de orders.get (url, max_retries = num_max_retries)).
paragbaxi
11
@paragbaxi: e ainda melhor arequests.get(url, max_retries=num_max_retries, dely_between_retries=3))
WoJ 27/02
1
@WoJ Eu peguei seus exemplos e os tornei realidade;) no just.gete just.postno github.com/kootenpv/just
PascalVKooten
2
Artigo útil sobre novas tentativas com pedidos: peterbe.com/plog/best-practice-with-retries-with-requests
Gokul

Respostas:

161

É a urllib3biblioteca subjacente que faz a nova tentativa. Para definir uma contagem máxima de novas tentativas, use adaptadores de transporte alternativos :

from requests.adapters import HTTPAdapter

s = requests.Session()
s.mount('http://stackoverflow.com', HTTPAdapter(max_retries=5))

O max_retriesargumento aceita um número inteiro ou um Retry()objeto ; o último fornece controle refinado sobre que tipos de falhas são tentadas novamente (um valor inteiro é transformado em uma Retry()instância que lida apenas com falhas de conexão; os erros após a conexão ser feita, por padrão, não são tratados, pois podem levar a efeitos colaterais) .


Resposta antiga, anterior à liberação dos pedidos 1.2.1 :

A requestsbiblioteca realmente não torna isso configurável, nem pretende (consulte esta solicitação de recebimento ). Atualmente (solicitações 1.1), a contagem de novas tentativas é definida como 0. Se você realmente deseja configurá-lo para um valor mais alto, precisará configurá-lo globalmente:

import requests

requests.adapters.DEFAULT_RETRIES = 5

Essa constante não está documentada; use-o por sua própria conta e risco, pois lançamentos futuros podem mudar a forma como isso é tratado.

Atualização : e isso fez a mudança; na versão 1.2.1, foi adicionada a opção de definir o max_retriesparâmetro na HTTPAdapter()classe , para que agora você precise usar adaptadores de transporte alternativos, veja acima. A abordagem de correção de macacos não funciona mais, a menos que você também corrija os HTTPAdapter.__init__()padrões (muito pouco recomendado).

Martijn Pieters
fonte
9
Você não precisa especificar isso para todos os sites, se isso não for necessário. Você pode fazer session.mount('http://', HTTPAdapter(max_retries=10))isso apenas para todas as conexões http. O mesmo com https funcionará para todas as conexões https.
user136036
1
@ user136036: sim, os adaptadores são procurados pela correspondência de prefixo mais longa; se você deseja que isso se aplique a todos os URLs http://e https://seja o prefixo mínimo a ser usado, consulte a documentação à qual a resposta está vinculada.
Martijn Pieters
1
Observe que HTTPAdapter(max_retries=5)funcionará apenas para determinado cenário. No documento de solicitações , Note, this applies only to failed DNS lookups, socket connections and connection timeouts, never to requests where data has made it to the server. By default, Requests does not retry failed connections.para forçar a tentativa de qualquer código de status, consulte a resposta de @ datashaman abaixo.
Steven Xu
@StevenXu: sim, você pode configurar Retry()para alterar quais cenários de falha são tentados novamente.
Martijn Pieters
226

Isso não apenas altera o max_retries, mas também habilita uma estratégia de retirada que faz com que as solicitações para todos os endereços http: // sejam suspensas por um período de tempo antes de tentar novamente (no total 5 vezes):

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

s = requests.Session()

retries = Retry(total=5,
                backoff_factor=0.1,
                status_forcelist=[ 500, 502, 503, 504 ])

s.mount('http://', HTTPAdapter(max_retries=retries))

s.get('http://httpstat.us/500')

Conforme a documentação paraRetry : se o backoff_factor for 0.1 , sleep () ficará suspenso por [0.1s, 0.2s, 0.4s, ...] entre tentativas. Também forçará uma nova tentativa se o código de status retornado for 500 , 502 , 503 ou 504 .

Várias outras opções para Retrypermitir um controle mais granular:

  • total - número total de tentativas a serem permitidas.
  • connect - Quantos erros relacionados à conexão devem ser tentados novamente.
  • read - Quantas vezes para tentar novamente os erros de leitura.
  • redirecionar - quantos redirecionamentos executar.
  • method_whitelist - Conjunto de verbos de método HTTP em maiúsculas nos quais devemos tentar novamente.
  • status_forcelist - Um conjunto de códigos de status HTTP nos quais devemos forçar uma nova tentativa.
  • backoff_factor - Um fator de retirada para aplicar entre tentativas.
  • raise_on_redirect - Se, se o número de redirecionamentos estiver esgotado, gerar um MaxRetryErrorou retornar uma resposta com um código de resposta no intervalo 3xx .
  • raise_on_status - significado semelhante ao raise_on_redirect : se devemos gerar uma exceção ou retornar uma resposta, se o status cair no intervalo status_forcelist e as tentativas foram esgotadas.

NB : raise_on_status é relativamente novo e ainda não foi lançado em urllib3 ou solicitações. Oargumento da palavra-chave raise_on_status parece ter entrado na biblioteca padrão no máximo na versão 3.6 do python.

Para fazer solicitações novamente em códigos de status HTTP específicos, use status_forcelist . Por exemplo, status_forcelist = [503] tentará novamente o código de status 503 (serviço indisponível).

Por padrão, a nova tentativa é acionada apenas para estas condições:

  • Não foi possível obter uma conexão da piscina.
  • TimeoutError
  • HTTPExceptiongerado (de http.client no Python 3 else activationplib ). Parece haver exceções HTTP de baixo nível, como URL ou protocolo não formado corretamente.
  • SocketError
  • ProtocolError

Observe que essas são todas as exceções que impedem que uma resposta HTTP regular seja recebida. Se qualquer resposta regular for gerada, nenhuma nova tentativa será feita. Sem usar o status_forcelist , mesmo uma resposta com status 500 não será tentada novamente.

Para que ele se comporte de uma maneira mais intuitiva para trabalhar com uma API ou servidor da Web remoto, eu usaria o snippet de código acima, que força tentativas nos status 500 , 502 , 503 e 504 , os quais não são incomuns no web e (possivelmente) recuperável, devido a um período de retirada suficientemente grande.

EDITADO : Importar Retryclasse diretamente do urllib3 .

datashaman
fonte
1
Estou tentando implementar sua lógica, mas não sei se está funcionando porque o log mostra apenas uma solicitação, mesmo o status res é 503. Como posso saber se a nova tentativa está funcionando? Veja o código: pastebin.com/rty4bKTw
Danilo Oliveira
1
O código anexado funciona conforme o esperado. O truque é o parâmetro status_forcelist . Isso informa ao pacote urllib3 para tentar novamente códigos de status específicos. Código: pastebin.com/k2bFbH7Z
datashaman
1
O urllib3 não pensa (e não deve) que o status 503 seja uma exceção (por padrão).
Datashaman
1
@Connor não, o adaptador está conectado à sessão.
Datashaman 30/08/19
1
O urlib3.Retry não faz mais parte dos pedidos. isso tem que ser importado diretamente.
Edição
59

Cuidado, a resposta de Martijn Pieters não é adequada para a versão 1.2.1+. Você não pode configurá-lo globalmente sem corrigir a biblioteca.

Você pode fazer isso:

import requests
from requests.adapters import HTTPAdapter

s = requests.Session()
s.mount('http://www.github.com', HTTPAdapter(max_retries=5))
s.mount('https://www.github.com', HTTPAdapter(max_retries=5))
gizmondo
fonte
22
Solução agradável, mas observe que não há atraso entre novas tentativas. Se você quiser dormir entre as tentativas, precisará rolar sozinho.
Nofinator 2/10/2013
18

Depois de lutar um pouco com algumas das respostas aqui, encontrei uma biblioteca chamada backoff que funcionava melhor para a minha situação. Um exemplo básico:

import backoff

@backoff.on_exception(
    backoff.expo,
    requests.exceptions.RequestException,
    max_tries=5,
    giveup=lambda e: e.response is not None and e.response.status_code < 500
)
def publish(self, data):
    r = requests.post(url, timeout=10, json=data)
    r.raise_for_status()

Eu ainda recomendo dar uma chance à funcionalidade nativa da biblioteca, mas se você tiver algum problema ou precisar de um controle mais amplo, o backoff é uma opção.

Brad Koch
fonte
1
ótima biblioteca, obrigado! Eu precisava dessa funcionalidade para outra coisa requests, então isso funciona perfeitamente!
Dennis Golomazov 02/11/19
3

Uma maneira mais limpa de obter maior controle pode ser empacotar o material de repetição em uma função e tornar essa função recuperável usando um decorador e colocar as exceções na lista de permissões.

Eu criei o mesmo aqui: http://www.praddy.in/retry-decorator-whitelisted-exceptions/

Reproduzindo o código nesse link:

def retry(exceptions, delay=0, times=2):
"""
A decorator for retrying a function call with a specified delay in case of a set of exceptions

Parameter List
-------------
:param exceptions:  A tuple of all exceptions that need to be caught for retry
                                    e.g. retry(exception_list = (Timeout, Readtimeout))
:param delay: Amount of delay (seconds) needed between successive retries.
:param times: no of times the function should be retried


"""
def outer_wrapper(function):
    @functools.wraps(function)
    def inner_wrapper(*args, **kwargs):
        final_excep = None  
        for counter in xrange(times):
            if counter > 0:
                time.sleep(delay)
            final_excep = None
            try:
                value = function(*args, **kwargs)
                return value
            except (exceptions) as e:
                final_excep = e
                pass #or log it

        if final_excep is not None:
            raise final_excep
    return inner_wrapper

return outer_wrapper

@retry(exceptions=(TimeoutError, ConnectTimeoutError), delay=0, times=3)
def call_api():
praddy
fonte