From b35ea3745484cebd0ad10ed3a219f39f052d145d Mon Sep 17 00:00:00 2001 From: Vincent Ardisson Date: Fri, 3 Jul 2020 15:57:37 +0200 Subject: [PATCH] [lcl] improve code style --- modules/lcl/browser.py | 45 +++- modules/lcl/enterprise/browser.py | 23 ++- modules/lcl/enterprise/pages.py | 17 +- modules/lcl/module.py | 49 +++-- modules/lcl/pages.py | 327 ++++++++++++++++++++++-------- 5 files changed, 340 insertions(+), 121 deletions(-) diff --git a/modules/lcl/browser.py b/modules/lcl/browser.py index de496b1c85..9d16ef5e44 100644 --- a/modules/lcl/browser.py +++ b/modules/lcl/browser.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . +# flake8: compatible from __future__ import unicode_literals @@ -26,7 +27,6 @@ from functools import wraps from dateutil.relativedelta import relativedelta - from weboob.exceptions import ( BrowserIncorrectPassword, BrowserUnavailable, AuthMethodNotImplemented, ActionNeeded, @@ -137,7 +137,10 @@ class LCLBrowser(LoginBrowser, StatesMixin): av_list = URL(r'https://assurance-vie-et-prevoyance.secure.lcl.fr/rest/assurance/synthesePartenaire', AVListPage) avdetail = URL(r'https://assurance-vie-et-prevoyance.secure.lcl.fr/consultation/epargne', AVDetailPage) av_history = URL(r'https://assurance-vie-et-prevoyance.secure.lcl.fr/rest/assurance/historique', AVHistoryPage) - av_investments = URL(r'https://assurance-vie-et-prevoyance.secure.lcl.fr/rest/detailEpargne/contrat/(?P\w+)', AVInvestmentsPage) + av_investments = URL( + r'https://assurance-vie-et-prevoyance.secure.lcl.fr/rest/detailEpargne/contrat/(?P\w+)', + AVInvestmentsPage + ) loans = URL(r'/outil/UWCR/SynthesePar/', LoansPage) loans_pro = URL(r'/outil/UWCR/SynthesePro/', LoansProPage) @@ -168,8 +171,11 @@ class LCLBrowser(LoginBrowser, StatesMixin): profile = URL(r'/outil/UWIP/Accueil/rafraichir', ProfilePage) - deposit = URL(r'/outil/UWPL/CompteATerme/accesSynthese', - r'/outil/UWPL/DetailCompteATerme/accesDetail', DepositPage) + deposit = URL( + r'/outil/UWPL/CompteATerme/accesSynthese', + r'/outil/UWPL/DetailCompteATerme/accesDetail', + DepositPage + ) __states__ = ('contracts', 'current_contract', 'parsed_contracts') @@ -371,16 +377,26 @@ def get_accounts(self): continue self.location('/outil/UWRI/Accueil/') + if self.no_perm.is_here(): self.logger.warning('RIB is unavailable.') + elif self.page.has_iban_choice(): self.rib.go(data={'compte': '%s/%s/%s' % (a.id[0:5], a.id[5:11], a.id[11:])}) if self.rib.is_here(): iban = self.page.get_iban() - a.iban = iban if iban and a.id[11:] in iban else NotAvailable + if iban and a.id[11:] in iban: + a.iban = iban + else: + a.iban = NotAvailable + else: iban = self.page.check_iban_by_account(a.id) - a.iban = iban if iban is not None else NotAvailable + if iban: + a.iban = iban + else: + a.iban = NotAvailable + self.update_accounts(a) # retrieve loans accounts @@ -460,7 +476,11 @@ def set_ownership(self, account, owner_name): if not account.ownership: if account.parent and account.parent.ownership: account.ownership = account.parent.ownership - elif re.search(r'(m|mr|me|mme|mlle|mle|ml)\.? (.*)\bou (m|mr|me|mme|mlle|mle|ml)\b(.*)', account.label, re.IGNORECASE): + elif re.search( + r'(m|mr|me|mme|mlle|mle|ml)\.? (.*)\bou (m|mr|me|mme|mlle|mle|ml)\b(.*)', + account.label, + re.IGNORECASE + ): account.ownership = AccountOwnership.CO_OWNER elif all(n in account.label for n in owner_name.split()): account.ownership = AccountOwnership.OWNER @@ -479,10 +499,13 @@ def get_bourse_accounts_ids(self): def get_history(self, account): if hasattr(account, '_market_link') and account._market_link: self.connexion_bourse() - self.location(account._link_id, params={ - 'nump': account._market_id, - }) + self.location( + account._link_id, params={ + 'nump': account._market_id, + } + ) self.page.get_fullhistory() + for tr in self.page.iter_history(): yield tr self.deconnexion_bourse() @@ -682,7 +705,7 @@ def init_new_recipient(self, recipient, **params): # Send sms to user. data = [ ('telChoisi', 'MOBILE'), - ('_', int(round(time.time() * 1000))) + ('_', int(round(time.time() * 1000))), ] self.location('/outil/UWAF/Otp/envoiCodeOtp', params=data) self.page.check_error() diff --git a/modules/lcl/enterprise/browser.py b/modules/lcl/enterprise/browser.py index 8bc424a40d..0b50f7a361 100644 --- a/modules/lcl/enterprise/browser.py +++ b/modules/lcl/enterprise/browser.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . +# flake8: compatible from weboob.browser import LoginBrowser, URL, need_login from weboob.exceptions import BrowserIncorrectPassword @@ -29,10 +30,16 @@ class LCLEnterpriseBrowser(LoginBrowser): BASEURL = 'https://entreprises.secure.lcl.fr' pass_expired = URL('/outil/IQEN/Authentication/forcerChangePassword', PassExpiredPage) - login = URL('/outil/IQEN/Authentication/indexRedirect', - '/outil/IQEN/Authentication/(?P.*)', LoginPage) - movements = URL('/outil/IQMT/mvt.Synthese/syntheseMouvementPerso', - '/outil/IQMT/mvt.Synthese', MovementsPage) + login = URL( + '/outil/IQEN/Authentication/indexRedirect', + '/outil/IQEN/Authentication/(?P.*)', + LoginPage + ) + movements = URL( + '/outil/IQMT/mvt.Synthese/syntheseMouvementPerso', + '/outil/IQMT/mvt.Synthese', + MovementsPage + ) profile = URL('/outil/IQGA/FicheUtilisateur/maFicheUtilisateur', ProfilePage) def __init__(self, *args, **kwargs): @@ -40,7 +47,6 @@ def __init__(self, *args, **kwargs): self.accounts = None self.owner_type = AccountOwnerType.ORGANIZATION - def deinit(self): if self.page and self.page.logged: self.login.go(page="logout") @@ -51,10 +57,11 @@ def deinit(self): def do_login(self): self.login.go().login(self.username, self.password) - error = self.page.get_error() if self.login.is_here() else False + if self.login.is_here(): + error = self.page.get_error() - if error: - raise BrowserIncorrectPassword(error) + if error: + raise BrowserIncorrectPassword(error) @need_login def get_accounts_list(self): diff --git a/modules/lcl/enterprise/pages.py b/modules/lcl/enterprise/pages.py index 147ada6e5c..a368a74395 100644 --- a/modules/lcl/enterprise/pages.py +++ b/modules/lcl/enterprise/pages.py @@ -18,8 +18,9 @@ # along with this weboob module. If not, see . -import re, requests +import re +import requests from weboob.browser.pages import HTMLPage, LoggedPage, pagination from weboob.browser.elements import ListElement, ItemElement, TableElement, method from weboob.browser.filters.standard import CleanText, Date, CleanDecimal, Env @@ -52,7 +53,7 @@ def login(self, login, password): class MovementsPage(LoggedPage, HTMLPage): def get_changecompte(self, link): form = self.get_form('//form[contains(@action, "changeCompte")]') - m = re.search('\'(\d+).*\'(\d+)', link) + m = re.search(r"'(\d+).*'(\d+)", link) form['perimetreMandatParentData'] = m.group(1) form['perimetreMandatEnfantData'] = m.group(2) # Can't do multi with async because of inconsistency... @@ -93,14 +94,16 @@ def obj_label(self): obj__data = Env('data') def parse(self, el): - page, url, data = self.page.get_changecompte(Link('.')(self)) if self.env['multi'] else (self.page, None, None) + page, url, data = (self.page, None, None) + if self.env['multi']: + page, url, data = self.page.get_changecompte(Link('.')(self)) + balance_xpath = '//div[contains(text(),"Solde")]/strong' self.env['balance'] = MyDecimal().filter(page.doc.xpath(balance_xpath)) self.env['currency'] = Account.get_currency(CleanText().filter(page.doc.xpath(balance_xpath))) self.env['url'] = url self.env['data'] = data - @pagination @method class iter_history(TableElement): @@ -114,7 +117,7 @@ class iter_history(TableElement): def next_page(self): url = Link('//a[contains(text(), "Page suivante")]', default=None)(self) if url: - m = re.search('\s+\'([^\']+).*\'(\d+)', url) + m = re.search(r"\s+'([^']+).*'(\d+)", url) return requests.Request("POST", m.group(1), data={'numPage': m.group(2)}) class item(ItemElement): @@ -127,7 +130,9 @@ class item(ItemElement): def obj_amount(self): credit = MyDecimal(TableCell('credit'))(self) debit = MyDecimal(TableCell('debit'))(self) - return credit if credit else -debit + if credit: + return credit + return -debit class ProfilePage(LoggedPage, HTMLPage): diff --git a/modules/lcl/module.py b/modules/lcl/module.py index c91d9bce55..4fbad1da98 100644 --- a/modules/lcl/module.py +++ b/modules/lcl/module.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . +# flake8: compatible from decimal import Decimal from functools import wraps @@ -65,31 +66,45 @@ class LCLModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapContact, VERSION = '2.1' DESCRIPTION = u'LCL' LICENSE = 'LGPLv3+' - CONFIG = BackendConfig(ValueBackendPassword('login', label='Identifiant', masked=False), - ValueBackendPassword('password', label='Code personnel'), - Value('website', label='Type de compte', default='par', - choices={'par': 'Particuliers', - 'pro': 'Professionnels', - 'ent': 'Entreprises', - 'esp': 'Espace Pro'}, - aliases={'elcl': 'par'})) + CONFIG = BackendConfig( + ValueBackendPassword('login', label='Identifiant', masked=False), + ValueBackendPassword('password', label='Code personnel'), + Value( + 'website', + label='Type de compte', + default='par', + choices={ + 'par': 'Particuliers', + 'pro': 'Professionnels', + 'ent': 'Entreprises', + 'esp': 'Espace Pro', + }, + aliases={'elcl': 'par'} + ) + ) BROWSER = LCLBrowser accepted_document_types = (DocumentTypes.STATEMENT, DocumentTypes.NOTICE, DocumentTypes.REPORT, DocumentTypes.OTHER) def create_default_browser(self): # assume all `website` option choices are defined here - browsers = {'par': LCLBrowser, - 'pro': LCLProBrowser, - 'ent': LCLEnterpriseBrowser, - 'esp': LCLEspaceProBrowser} + browsers = { + 'par': LCLBrowser, + 'pro': LCLProBrowser, + 'ent': LCLEnterpriseBrowser, + 'esp': LCLEspaceProBrowser, + } website_value = self.config['website'] - self.BROWSER = browsers.get(website_value.get(), - browsers[website_value.default]) - - return self.create_browser(self.config['login'].get(), - self.config['password'].get()) + self.BROWSER = browsers.get( + website_value.get(), + browsers[website_value.default] + ) + + return self.create_browser( + self.config['login'].get(), + self.config['password'].get() + ) def iter_accounts(self): return self.browser.get_accounts_list() diff --git a/modules/lcl/pages.py b/modules/lcl/pages.py index 84f1dc0c48..7736e7a03b 100644 --- a/modules/lcl/pages.py +++ b/modules/lcl/pages.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . +# flake8: compatible + from __future__ import unicode_literals, division import re @@ -112,7 +114,7 @@ class LCLVirtKeyboard(MappedVirtKeyboard): '6': 'aba912172f21f78cd6da437cfc4cdbd0', '7': 'f710190d6b947869879ec02d8e851dfa', '8': 'b42cc25e1539a15f767aa7a641f3bfec', - '9': 'cc60e5894a9d8e12ee0c2c104c1d5490' + '9': 'cc60e5894a9d8e12ee0c2c104c1d5490', } url = "/outil/UAUT/Clavier/creationClavier?random=" @@ -344,13 +346,25 @@ class get_advisor(ItemElement): obj_name = CleanText('//div[@id="contacterMaBqMenu"]//p[@id="itemNomContactMaBq"]/span') obj_email = obj_mobile = obj_fax = NotAvailable - obj_phone = Regexp(CleanText('//div[@id="contacterMaBqMenu"]//p[contains(text(), "Tel")]', replace=[(' ', '')]), '([\s\d]+)', default=NotAvailable) + obj_phone = Regexp( + CleanText('//div[@id="contacterMaBqMenu"]//p[contains(text(), "Tel")]', replace=[(' ', '')]), + r'([\s\d]+)', + default=NotAvailable + ) obj_agency = CleanText('//div[@id="sousContentAgence"]//p[@class="itemSousTitreMenuMaBq"][1]') def obj_address(self): - address = CleanText('//div[@id="sousContentAgence"]//p[@class="itemSousTitreMenuMaBq"][2]', default=None)(self) - city = CleanText('//div[@id="sousContentAgence"]//p[@class="itemSousTitreMenuMaBq"][3]', default=None)(self) - return "%s %s" % (address, city) if address and city else NotAvailable + address = CleanText( + '//div[@id="sousContentAgence"]//p[@class="itemSousTitreMenuMaBq"][2]', + default=None + )(self) + city = CleanText( + '//div[@id="sousContentAgence"]//p[@class="itemSousTitreMenuMaBq"][3]', + default=None + )(self) + if not (address and city): + return NotAvailable + return "%s %s" % (address, city) class LoansPage(LoggedPage, HTMLPage): @@ -375,20 +389,37 @@ class account(ItemElement): def obj_label(self): has_type = CleanText('./ancestor::table[.//th[contains(text(), "Type")]]', default=None)(self) - return CleanText('./td[2]')(self) if has_type else CleanText('./ancestor::table/preceding-sibling::div[1]')(self).split(' - ')[0] + if has_type: + return CleanText('./td[2]')(self) + else: + return CleanText('./ancestor::table/preceding-sibling::div[1]')(self).split(' - ')[0] def obj_ownership(self): - if re.search(r'(m|mr|me|mme|mlle|mle|ml)\.? (.*)\b(ou)? (m|mr|me|mme|mlle|mle|ml)\b(.*)', CleanText(TableCell('id'))(self), re.IGNORECASE): + pattern = re.compile( + r'(m|mr|me|mme|mlle|mle|ml)\.? (.*)\b(ou)? (m|mr|me|mme|mlle|mle|ml)\b(.*)', + re.IGNORECASE + ) + if pattern.search(CleanText(TableCell('id'))(self)): return AccountOwnership.CO_OWNER return AccountOwnership.OWNER def parse(self, el): label = Field('label')(self) - trs = self.xpath('//td[contains(text(), $label)]/ancestor::tr[1] | ./ancestor::table[1]/tbody/tr', label=label) + trs = self.xpath( + '//td[contains(text(), $label)]/ancestor::tr[1] | ./ancestor::table[1]/tbody/tr', + label=label + ) i = [i for i in range(len(trs)) if el == trs[i]] - i = i[0] if i else 0 + if i: + i = i[0] + else: + i = 0 label = label.replace(' ', '') - self.env['id'] = "%s%s%s" % (Regexp(CleanText(TableCell('id')), r'(\w+)\s-\s(\w+)', r'\1\2')(self), label.replace(' ', ''), i) + self.env['id'] = "%s%s%s" % ( + Regexp(CleanText(TableCell('id')), r'(\w+)\s-\s(\w+)', r'\1\2')(self), + label.replace(' ', ''), + i, + ) class LoansProPage(LoggedPage, HTMLPage): @@ -413,43 +444,65 @@ class account(ItemElement): def obj_label(self): has_type = CleanText('./ancestor::table[.//th[contains(text(), "Nature libell")]]', default=None)(self) - return CleanText('./td[3]')(self) if has_type else CleanText('./ancestor::table/preceding-sibling::div[1]')(self).split(' - ')[0] + if has_type: + return CleanText('./td[3]')(self) + else: + return CleanText('./ancestor::table/preceding-sibling::div[1]')(self).split(' - ')[0] def parse(self, el): label = Field('label')(self) - trs = self.xpath('//td[contains(text(), $label)]/ancestor::tr[1] | ./ancestor::table[1]/tbody/tr', label=label) + trs = self.xpath( + '//td[contains(text(), $label)]/ancestor::tr[1] | ./ancestor::table[1]/tbody/tr', + label=label + ) i = [i for i in range(len(trs)) if el == trs[i]] - i = i[0] if i else 0 + if i: + i = i[0] + else: + i = 0 label = label.replace(' ', '') - self.env['id'] = "%s%s%s" % (Regexp(CleanText(TableCell('id')), r'(\w+)\s-\s(\w+)', r'\1\2')(self), label.replace(' ', ''), i) + self.env['id'] = "%s%s%s" % ( + Regexp(CleanText(TableCell('id')), r'(\w+)\s-\s(\w+)', r'\1\2')(self), + label.replace(' ', ''), + i, + ) class Transaction(FrenchTransaction): PATTERNS = [ - (re.compile('^(?PCB) (?PRETRAIT) DU (?P
\d+)/(?P\d+)'), FrenchTransaction.TYPE_WITHDRAWAL), - (re.compile('^(?P(PRLV|PE)( SEPA)?) (?P.*)'), FrenchTransaction.TYPE_ORDER), - (re.compile('^(?PCHQ\.) (?P.*)'), FrenchTransaction.TYPE_CHECK), - (re.compile('^(?PRELEVE CB) AU (\d+)/(\d+)/(\d+)'), FrenchTransaction.TYPE_CARD), - (re.compile('^(?PCB) (?P.*) (?P
\d+)/(?P\d+)/(?P\d+)'), FrenchTransaction.TYPE_CARD), - (re.compile('^(?P(PRELEVEMENT|TELEREGLEMENT|TIP)) (?P.*)'), FrenchTransaction.TYPE_ORDER), - (re.compile('^(?P(ECHEANCE\s*)?PRET)(?P.*)'), FrenchTransaction.TYPE_LOAN_PAYMENT), - (re.compile('^(TP-\d+-)?(?P(EVI|VIR(EM(EN)?)?T?)(.PERMANENT)? ((RECU|FAVEUR) TIERS|SEPA RECU)?)( /FRM)?(?P.*)'), FrenchTransaction.TYPE_TRANSFER), - (re.compile('^(?PREMBOURST)(?P.*)'), FrenchTransaction.TYPE_PAYBACK), - (re.compile('^(?PCOM(MISSIONS?)?)(?P.*)'), FrenchTransaction.TYPE_BANK), - (re.compile('^(?P(?PREMUNERATION).*)'), FrenchTransaction.TYPE_BANK), - (re.compile('^(?P(?PABON.*?)\s*.*)'), FrenchTransaction.TYPE_BANK), - (re.compile('^(?P(?PRESULTAT .*?)\s*.*)'), FrenchTransaction.TYPE_BANK), - (re.compile('^(?P(?PTRAIT\..*?)\s*.*)'), FrenchTransaction.TYPE_BANK), + ( + re.compile(r'^(?PCB) (?PRETRAIT) DU (?P
\d+)/(?P\d+)'), + FrenchTransaction.TYPE_WITHDRAWAL, + ), + (re.compile(r'^(?P(PRLV|PE)( SEPA)?) (?P.*)'), FrenchTransaction.TYPE_ORDER), + (re.compile(r'^(?PCHQ\.) (?P.*)'), FrenchTransaction.TYPE_CHECK), + (re.compile(r'^(?PRELEVE CB) AU (\d+)/(\d+)/(\d+)'), FrenchTransaction.TYPE_CARD), + ( + re.compile(r'^(?PCB) (?P.*) (?P
\d+)/(?P\d+)/(?P\d+)'), + FrenchTransaction.TYPE_CARD, + ), + (re.compile(r'^(?P(PRELEVEMENT|TELEREGLEMENT|TIP)) (?P.*)'), FrenchTransaction.TYPE_ORDER), + (re.compile(r'^(?P(ECHEANCE\s*)?PRET)(?P.*)'), FrenchTransaction.TYPE_LOAN_PAYMENT), + ( + re.compile(r'^(TP-\d+-)?(?P(EVI|VIR(EM(EN)?)?T?)(.PERMANENT)? ((RECU|FAVEUR) TIERS|SEPA RECU)?)( /FRM)?(?P.*)'), + FrenchTransaction.TYPE_TRANSFER, + ), + (re.compile(r'^(?PREMBOURST)(?P.*)'), FrenchTransaction.TYPE_PAYBACK), + (re.compile(r'^(?PCOM(MISSIONS?)?)(?P.*)'), FrenchTransaction.TYPE_BANK), + (re.compile(r'^(?P(?PREMUNERATION).*)'), FrenchTransaction.TYPE_BANK), + (re.compile(r'^(?P(?PABON.*?)\s*.*)'), FrenchTransaction.TYPE_BANK), + (re.compile(r'^(?P(?PRESULTAT .*?)\s*.*)'), FrenchTransaction.TYPE_BANK), + (re.compile(r'^(?P(?PTRAIT\..*?)\s*.*)'), FrenchTransaction.TYPE_BANK), (re.compile(r'(?P(?PCOTISATION).*)'), FrenchTransaction.TYPE_BANK), (re.compile(r'(?P(?PINTERETS).*)'), FrenchTransaction.TYPE_BANK), - (re.compile('^(?PREM CHQ) (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), - (re.compile('^VIREMENT.*'), FrenchTransaction.TYPE_TRANSFER), - (re.compile('.*(PRELEVEMENTS|PRELVT|TIP).*'), FrenchTransaction.TYPE_ORDER), - (re.compile('.*CHEQUE.*'), FrenchTransaction.TYPE_CHECK), - (re.compile('.*ESPECES.*'), FrenchTransaction.TYPE_DEPOSIT), - (re.compile('.*(CARTE|CB).*'), FrenchTransaction.TYPE_CARD), - (re.compile('.*(AGIOS|ANNULATIONS|IMPAYES|CREDIT).*'), FrenchTransaction.TYPE_BANK), - (re.compile('.*(FRAIS DE TENUE DE COMPTE).*'), FrenchTransaction.TYPE_BANK), + (re.compile(r'^(?PREM CHQ) (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), + (re.compile(r'^VIREMENT.*'), FrenchTransaction.TYPE_TRANSFER), + (re.compile(r'.*(PRELEVEMENTS|PRELVT|TIP).*'), FrenchTransaction.TYPE_ORDER), + (re.compile(r'.*CHEQUE.*'), FrenchTransaction.TYPE_CHECK), + (re.compile(r'.*ESPECES.*'), FrenchTransaction.TYPE_DEPOSIT), + (re.compile(r'.*(CARTE|CB).*'), FrenchTransaction.TYPE_CARD), + (re.compile(r'.*(AGIOS|ANNULATIONS|IMPAYES|CREDIT).*'), FrenchTransaction.TYPE_BANK), + (re.compile(r'.*(FRAIS DE TENUE DE COMPTE).*'), FrenchTransaction.TYPE_BANK), (re.compile(r'.*\b(RETRAIT)\b.*'), FrenchTransaction.TYPE_WITHDRAWAL), ] @@ -501,58 +554,124 @@ def load_details(self): def obj_rdate(self): rdate = self.obj.rdate date = Field('date')(self) + if rdate > date: date_guesser = Env('date_guesser')(self) return date_guesser.guess_date(rdate.day, rdate.month) + return rdate def obj_type(self): - type = Async('details', CleanText(u'//td[contains(text(), "Nature de l\'opération")]/following-sibling::*[1]'))(self) + type = Async( + 'details', + CleanText("""//td[contains(text(), "Nature de l'opération")]/following-sibling::*[1]""") + )(self) if not type: return Transaction.TYPE_UNKNOWN + for pattern, _type in Transaction.PATTERNS: match = pattern.match(type) if match: return _type + return Transaction.TYPE_UNKNOWN def condition(self): - return (self.parent.get_colnum('date') is not None - and len(self.el.findall('td')) >= 3 - and self.el.get('class') - and 'tableTr' not in self.el.get('class')) + return ( + self.parent.get_colnum('date') is not None + and len(self.el.findall('td')) >= 3 + and self.el.get('class') + and 'tableTr' not in self.el.get('class') + ) def validate(self, obj): if obj.category == 'RELEVE CB': obj.type = Transaction.TYPE_CARD_SUMMARY - raw = Async('details', CleanText(u'//td[contains(text(), "Libellé")]/following-sibling::*[1]|//td[contains(text(), "Nom du donneur")]/following-sibling::*[1]', default=obj.raw))(self) + raw = Async( + 'details', + CleanText( + '//td[contains(text(), "Libellé")]/following-sibling::*[1]|//td[contains(text(), "Nom du donneur")]/following-sibling::*[1]', + default=obj.raw + ) + )(self) + if raw: - if obj.raw in raw or raw in obj.raw or ' ' not in obj.raw: + if ( + obj.raw in raw + or raw in obj.raw + or ' ' not in obj.raw + ): obj.raw = raw obj.label = raw else: obj.label = '%s %s' % (obj.raw, raw) obj.raw = '%s %s' % (obj.raw, raw) + m = re.search(r'\d+,\d+COM (\d+,\d+)', raw) if m: obj.commission = -CleanDecimal(replace_dots=True).filter(m.group(1)) + elif not obj.raw: # Empty transaction label - obj.raw = obj.label = Async('details', CleanText(u'//td[contains(text(), "Nature de l\'opération")]/following-sibling::*[1]'))(self) + obj.raw = obj.label = Async( + 'details', + CleanText("""//td[contains(text(), "Nature de l'opération")]/following-sibling::*[1]""") + )(self) + # Some transactions have no details, but we can find the type of the transaction, # the label and the category from the raw label. if obj.type == Transaction.TYPE_UNKNOWN: parse_with_patterns(obj.raw, obj, self.klass.PATTERNS) + if not obj.date: - obj.date = Async('details', Date(CleanText(u'//td[contains(text(), "Date de l\'opération")]/following-sibling::*[1]', default=u''), dayfirst=True, default=NotAvailable))(self) + obj.date = Async( + 'details', + Date( + CleanText( + """//td[contains(text(), "Date de l'opération")]/following-sibling::*[1]""", + default='' + ), + dayfirst=True, + default=NotAvailable + ) + )(self) + obj.rdate = obj.date - obj.vdate = Async('details', Date(CleanText(u'//td[contains(text(), "Date de valeur")]/following-sibling::*[1]', default=u''), dayfirst=True, default=NotAvailable))(self) - obj.amount = Async('details', CleanDecimal(u'//td[contains(text(), "Montant")]/following-sibling::*[1]', replace_dots=True, default=NotAvailable))(self) + + obj.vdate = Async( + 'details', + Date( + CleanText( + '//td[contains(text(), "Date de valeur")]/following-sibling::*[1]', + default='' + ), + dayfirst=True, + default=NotAvailable + ) + )(self) + + obj.amount = Async( + 'details', + CleanDecimal( + '//td[contains(text(), "Montant")]/following-sibling::*[1]', + replace_dots=True, + default=NotAvailable + ) + )(self) + # ugly hack to fix broken html # sometimes transactions have really an amount of 0... if not obj.amount and CleanDecimal(TableCell('credit'), default=None)(self) is None: - obj.amount = Async('details', CleanDecimal(u'//td[contains(text(), "Montant")]/following-sibling::*[1]', replace_dots=True, default=NotAvailable))(self) + obj.amount = Async( + 'details', + CleanDecimal( + u'//td[contains(text(), "Montant")]/following-sibling::*[1]', + replace_dots=True, + default=NotAvailable + ) + )(self) + return True @pagination @@ -565,10 +684,15 @@ def get_balance_without_comings(self): default=NotAvailable )(self.doc) -class CardsPage(LoggedPage, HTMLPage): +class CardsPage(LoggedPage, HTMLPage): def deferred_date(self): - deferred_date = Regexp(CleanText('//div[@class="date"][contains(text(), "Carte")]'), r'le ([^:]+)', default=None)(self.doc) + deferred_date = Regexp( + CleanText('//div[@class="date"][contains(text(), "Carte")]'), + r'le ([^:]+)', + default=None + )(self.doc) + assert deferred_date, 'Cannot find deferred_date' return parse_french_date(deferred_date).date() @@ -726,7 +850,9 @@ def open_iframe(self): break def password_required(self): - return CleanText(u'//b[contains(text(), "Afin de sécuriser vos transactions, nous vous invitons à créer un mot de passe trading")]')(self.doc) + return CleanText( + '//b[contains(text(), "Afin de sécuriser vos transactions, nous vous invitons à créer un mot de passe trading")]' + )(self.doc) def get_next(self): if 'onload' in self.doc.xpath('.//body')[0].attrib: @@ -802,7 +928,10 @@ class item(ItemElement): klass = Investment obj_label = CleanText('.//td[2]/div/a') - obj_code = CleanText('.//td[2]/div/br/following-sibling::text()') & Regexp(pattern='^([^ ]+).*', default=NotAvailable) + obj_code = ( + CleanText('.//td[2]/div/br/following-sibling::text()') + & Regexp(pattern='^([^ ]+).*', default=NotAvailable) + ) obj_quantity = MyDecimal('.//td[3]/span') obj_diff = MyDecimal('.//td[7]/span') obj_valuation = MyDecimal('.//td[5]') @@ -838,8 +967,8 @@ class iter_history(TableElement): def next_page(self): form = self.page.get_form(id="historyFilter") form['PAGE'] = int(form['PAGE']) + 1 - return requests.Request("POST", form.url, data=dict(form)) \ - if self.page.doc.xpath('//*[@data-page = $page]', page=form['PAGE']) else None + if self.page.doc.xpath('//*[@data-page = $page]', page=form['PAGE']): + return requests.Request("POST", form.url, data=dict(form)) class item(ItemElement): klass = Transaction @@ -854,6 +983,8 @@ def obj_label(self): def parse(self, el): i = None + self.env['investments'] = [] + if CleanText(TableCell('code'))(self): i = Investment() i.label = Field('label')(self) @@ -861,7 +992,8 @@ def parse(self, el): i.quantity = MyDecimal(TableCell('quantity'))(self) i.valuation = Field('amount')(self) i.vdate = Field('date')(self) - self.env['investments'] = [i] if i else [] + + self.env['investments'] = [i] MARKET_ORDER_DIRECTIONS = { @@ -1002,7 +1134,10 @@ def get_calie_life_insurances_first_index(self): # so we stop at the first index for account in self.doc.xpath('//table[@class]/tbody/tr'): if account.xpath('.//td[has-class("nomContrat")]//a[contains(@class, "redirect")][@href="#"]'): - index = Attr(account.xpath('.//td[has-class("nomContrat")]//a[contains(@class, "redirect")][@href="#"]'), 'id')(self) + index = Attr( + account.xpath('.//td[has-class("nomContrat")]//a[contains(@class, "redirect")][@href="#"]'), # wtf + 'id' + )(self) return index @method @@ -1141,7 +1276,9 @@ def get_colnum(self, name): class CaliePage(LoggedPage, HTMLPage): def check_error(self): - message = CleanText('//div[contains(@class, "disclaimer-div")]//text()[contains(., "utilisation vaut acceptation")]')(self.doc) + message = CleanText( + '//div[contains(@class, "disclaimer-div")]//text()[contains(., "utilisation vaut acceptation")]' + )(self.doc) if self.doc.xpath('//button[@id="acceptDisclaimerButton"]') and message: raise ActionNeeded(message) @@ -1213,7 +1350,10 @@ def come_back(self): params['typeaction'] = 'reroutage_retour' params['site'] = 'LCLI' params['stbzn'] = 'bnc' - return self.browser.location('https://assurance-vie-et-prevoyance.secure.lcl.fr/filiale/entreeBam', params=params) + return self.browser.location( + 'https://assurance-vie-et-prevoyance.secure.lcl.fr/filiale/entreeBam', + params=params + ) class AVListPage(LoggedPage, JsonPage): @@ -1267,11 +1407,11 @@ class item(ItemElement): # 70= N° transaction, 6660666= N° account, 2018-03-18= date and 2018-03-16=rdate. # We thus use "70_ABC666ABC" for the transaction ID. - obj_id = Regexp(CleanText(Dict('idope')), '(\d+_[\dA-Z]+)') + obj_id = Regexp(CleanText(Dict('idope')), r'(\d+_[\dA-Z]+)') def obj__dates(self): raw = CleanText(Dict('idope'))(self) - m = re.findall('\d{4}-\d{2}-\d{2}', raw) + m = re.findall(r'\d{4}-\d{2}-\d{2}', raw) # We must verify that the two dates are correctly fetched assert len(m) == 2 return m @@ -1290,8 +1430,14 @@ def update_life_insurance_account(self, life_insurance): Dict('situationAdministrativeEpargne/lppeoscp'), Dict('situationAdministrativeEpargne/lnpeoscp'), )(self.doc) - life_insurance.label = '%s %s' % (Dict('situationAdministrativeEpargne/lcofc')(self.doc), life_insurance._owner) - life_insurance.valuation_diff = CleanDecimal(Dict('situationFinanciereEpargne/mtpmvcnt'), default=NotAvailable)(self.doc) + life_insurance.label = '%s %s' % ( + Dict('situationAdministrativeEpargne/lcofc')(self.doc), + life_insurance._owner, + ) + life_insurance.valuation_diff = CleanDecimal( + Dict('situationFinanciereEpargne/mtpmvcnt'), + default=NotAvailable + )(self.doc) return life_insurance @method @@ -1322,8 +1468,13 @@ def obj_code_type(self): class RibPage(LoggedPage, LCLBasePage): def get_iban(self): - if (self.doc.xpath('//div[contains(@class, "rib_cadre")]//div[contains(@class, "rib_internat")]')): - return CleanText('//div[contains(@class, "rib_cadre") and not(contains(@class, "hidden"))]//div[contains(@class, "rib_internat")]//p//strong/text()[1]', replace=[(' ', '')])(self.doc) + if self.doc.xpath( + '//div[contains(@class, "rib_cadre")]//div[contains(@class, "rib_internat")]' + ): + return CleanText( + '//div[contains(@class, "rib_cadre") and not(contains(@class, "hidden"))]//div[contains(@class, "rib_internat")]//p//strong/text()[1]', + replace=[(' ', '')] + )(self.doc) def check_iban_by_account(self, account_id): iban_account_id = CleanText().filter(self.doc.xpath('(//td[@class[contains(., "guichet-")]]/following-sibling::*)[1]/strong')) @@ -1331,11 +1482,17 @@ def check_iban_by_account(self, account_id): iban_account = "%s%s" % (iban_guichet_id, iban_account_id[4:]) if account_id == iban_account: - return CleanText('//div[contains(@class, "rib_cadre") and not(contains(@class, "hidden"))]//div[contains(@class, "rib_internat")]//p//strong/text()[1]', replace=[(' ', '')])(self.doc) + return CleanText( + '//div[contains(@class, "rib_cadre") and not(contains(@class, "hidden"))]//div[contains(@class, "rib_internat")]//p//strong/text()[1]', + replace=[(' ', '')] + )(self.doc) + return None def has_iban_choice(self): - return False if self.doc.xpath('(//strong[contains(., "RELEVE D\'IDENTITE BANCAIRE")])[1]') else True + return not bool( + self.doc.xpath('(//strong[contains(., "RELEVE D\'IDENTITE BANCAIRE")])[1]') + ) class HomePage(LoggedPage, HTMLPage): @@ -1347,7 +1504,9 @@ def on_load(self): # This aims to track input errors. script_error = CleanText(u"//script[contains(text(), 'if (\"true\"===\"true\")')]")(self.doc) if script_error: - raise TransferBankError(message=CleanText().filter(html2text(re.search(u'\.html\("(.*?)"\)', script_error).group(1)))) + html = re.search(r'\.html\("(.*?)"\)', script_error).group(1) + message = CleanText().filter(html2text(html)) # wtf? + raise TransferBankError(message=message) def can_transfer(self, account_transfer_id): for div in self.doc.xpath('//div[input[@id="indexCompteEmetteur"]]//div[@class="infoCompte" and not(@title)]'): @@ -1408,19 +1567,23 @@ def handle_response(self, account, recipient): transfer.account_iban = account.iban transfer.account_label = account.label transfer.account_balance = account.balance - assert account._transfer_id in CleanText( - u'//div[div[@class="libelleChoix" and contains(text(), "Compte émetteur")]] \ - //div[@class="infoCompte" and not(@title)]', replace=[(' ', '')] - )(self.doc) + assert ( + account._transfer_id in CleanText( + '//div[div[@class="libelleChoix" and contains(text(), "Compte émetteur")]]//div[@class="infoCompte" and not(@title)]', + replace=[(' ', '')] + )(self.doc) + ) transfer._recipient = recipient transfer.recipient_id = self.get_id_from_response('recipient') transfer.recipient_iban = recipient.iban transfer.recipient_label = recipient.label - assert recipient._transfer_id in CleanText( - u'//div[div[@class="libelleChoix" and contains(text(), "Compte destinataire")]] \ - //div[@class="infoCompte" and not(@title)]', replace=[(' ', '')] - )(self.doc) + assert ( + recipient._transfer_id in CleanText( + '//div[div[@class="libelleChoix" and contains(text(), "Compte destinataire")]]//div[@class="infoCompte" and not(@title)]', + replace=[(' ', '')] + )(self.doc) + ) transfer.currency = FrenchTransaction.Currency('//div[@class="topBox"]/div[@class="montant"]')(self.doc) transfer.amount = CleanDecimal('//div[@class="topBox"]/div[@class="montant"]', replace_dots=True)(self.doc) @@ -1429,8 +1592,10 @@ def handle_response(self, account, recipient): dayfirst=True )(self.doc) # skip html comment with filtering on text() content - transfer.label = CleanText('//div[@class="motif"]/text()[contains(., "Motif : ")]', - replace=[('Motif : ', '')])(self.doc) + transfer.label = CleanText( + '//div[@class="motif"]/text()[contains(., "Motif : ")]', + replace=[('Motif : ', '')] + )(self.doc) return transfer @@ -1441,7 +1606,7 @@ def confirm(self): def get_value(self, _id, value_type): for div in self.doc.xpath('//div[@onclick]'): if _id in CleanText('.//div[not(@title)]', replace=[(' ', '')])(div): - return Regexp(Attr('.', 'onclick'), '(\d+)')(div) + return Regexp(Attr('.', 'onclick'), r'(\d+)')(div) raise TransferError('Could not find %s account.' % value_type) def choose_origin(self, account_transfer_id): @@ -1476,7 +1641,11 @@ def parse(self, el): if bool(CleanText('./div[@id="soldeEurosCompte"]')(self)): self.env['category'] = u'Interne' account = find_object(self.page.browser.get_accounts_list(), id=self.obj_id(self)) - self.env['iban'] = account.iban if account else NotAvailable + + self.env['iban'] = NotAvailable + if account: + self.env['iban'] = account.iban + self.env['bank_name'] = u'LCL' else: self.env['category'] = u'Externe' -- GitLab