Newer
Older
# -*- coding: utf-8 -*-
# Copyright(C) 2017 Vincent A
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from contextlib import contextmanager
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
from functools import wraps
from .browsers import LoginBrowser
from .exceptions import LoggedOut
from ..exceptions import BrowserUnavailable
__all__ = ['login_method', 'retry_on_logout', 'RetryLoginBrowser']
def login_method(func):
"""Decorate a method to indicate the browser is logging in.
When the decorated method is called, pages like
`weboob.browser.pages.LoginPage` will not raise `LoggedOut`, since it is
expected for the browser not to be logged yet.
"""
func.login_decorated = True
@wraps(func)
def wrapper(browser, *args, **kwargs):
browser.logging_in += 1
try:
return func(browser, *args, **kwargs)
finally:
browser.logging_in -= 1
return wrapper
def retry_on_logout(exc_check=LoggedOut, tries=4):
"""Decorate a function to retry several times in case of exception.
The decorated function is called at max 4 times. It is retried only when it
raises an exception of the type `weboob.browser.exceptions.LoggedOut`.
If the function call succeeds and returns an iterator, a wrapper to the
iterator is returned. If iterating on the result raises a `LoggedOut`,
the iterator is recreated by re-calling the function, but the values
already yielded will not be re-yielded.
For consistency, the function MUST always return values in the same order.
Adding this decorator to a method which can be called from another
decorated method should be avoided, since nested calls will greatly
increase the number of retries.
"""
if not isinstance(exc_check, type) or not issubclass(exc_check, Exception):
raise TypeError('retry_on_logout() must be called in order to decorate %r' % tries)
def decorator(func):
@wraps(func)
def wrapper(browser, *args, **kwargs):
cb = lambda: func(browser, *args, **kwargs)
for i in range(tries, 0, -1):
try:
ret = cb()
except exc_check as exc:
browser.logger.info('%r raised, retrying', exc)
continue
if not (hasattr(ret, '__next__') or hasattr(ret, 'next')):
return ret # simple value, no need to retry on items
return iter_retry(cb, value=ret, remaining=i, exc_check=exc_check, logger=browser.logger)
raise BrowserUnavailable('Site did not reply successfully after multiple tries')
return wrapper
return decorator
@contextmanager
def retry_on_logout_context(tries=4, logger=None):
for i in range(tries, 0, -1):
try:
yield
except LoggedOut as exc:
if logger:
logger.debug('%r raised, retrying', exc)
else:
return
raise BrowserUnavailable('Site did not reply successfully after multiple tries')
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class RetryLoginBrowser(LoginBrowser):
"""Browser that can retry methods if the site logs out the session.
Some sites can terminate a session anytime, redirecting to a login page.
To avoid having to handle it in the middle of every method, one can simply
let logouts raise a `weboob.browser.exceptions.LoggedOut` exception that
is handled with a retry, thanks to the `@retry_on_logout` decorator.
The `weboob.browser.pages.LoginPage` will raise `LoggedOut` if the browser
is not currently logging in. To detect this situation, the `do_login`
method MUST be decorated with `@login_method`.
"""
def __init__(self, *args, **kwargs):
super(RetryLoginBrowser, self).__init__(*args, **kwargs)
self.logging_in = 0
if not hasattr(self.do_login, 'login_decorated'):
raise Exception('do_login method was not decorated with @login_method')
class iter_retry(object):
# when the callback is retried, it will create a new iterator, but we may already yielded
# some values, so we need to keep track of them and seek in the middle of the iterator
def __init__(self, cb, remaining=4, value=None, exc_check=LoggedOut, logger=None):
self.cb = cb
self.it = iter(value) if value is not None else None
self.items = []
self.remaining = remaining
self.logger = logger
self.exc_check = exc_check
def __iter__(self):
return self
def __next__(self):
if self.remaining <= 0:
raise BrowserUnavailable('Site did not reply successfully after multiple tries')
if self.it is None:
self.it = iter(self.cb())
# recreated iterator, consume previous items
try:
nb = -1
for nb, sent in enumerate(self.items):
new = next(self.it)
if hasattr(new, 'iter_fields'):
equal = dict(sent.iter_fields()) == dict(new.iter_fields())
else:
equal = sent == new
if not equal:
# safety is not guaranteed
raise BrowserUnavailable('Site replied inconsistently between retries, %r vs %r', sent, new)
except StopIteration:
raise BrowserUnavailable('Site replied fewer elements (%d) than last iteration (%d)', nb + 1, len(self.items))
except self.exc_check as exc:
if self.logger:
self.logger.info('%r raised, retrying', exc)
self.it = None
self.remaining -= 1
return next(self)
# return one item
try:
obj = next(self.it)
except self.exc_check as exc:
if self.logger:
self.logger.info('%r raised, retrying', exc)
self.it = None
self.remaining -= 1
return next(self)
else:
self.items.append(obj)
return obj
next = __next__