From e1db34da1ce124957e38ca31b1ebb8737c7c7e43 Mon Sep 17 00:00:00 2001 From: Romain Bignon Date: Thu, 20 Dec 2012 15:49:26 +0100 Subject: [PATCH] add barclays bank --- modules/barclays/__init__.py | 23 +++++ modules/barclays/backend.py | 67 ++++++++++++++ modules/barclays/browser.py | 110 +++++++++++++++++++++++ modules/barclays/favicon.png | Bin 0 -> 1642 bytes modules/barclays/pages.py | 170 +++++++++++++++++++++++++++++++++++ modules/barclays/test.py | 30 +++++++ 6 files changed, 400 insertions(+) create mode 100644 modules/barclays/__init__.py create mode 100644 modules/barclays/backend.py create mode 100644 modules/barclays/browser.py create mode 100644 modules/barclays/favicon.png create mode 100644 modules/barclays/pages.py create mode 100644 modules/barclays/test.py diff --git a/modules/barclays/__init__.py b/modules/barclays/__init__.py new file mode 100644 index 00000000..0a9e0b12 --- /dev/null +++ b/modules/barclays/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from .backend import BarclaysBackend + +__all__ = ['BarclaysBackend'] diff --git a/modules/barclays/backend.py b/modules/barclays/backend.py new file mode 100644 index 00000000..f286af66 --- /dev/null +++ b/modules/barclays/backend.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.capabilities.bank import ICapBank, AccountNotFound +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import Barclays + + +__all__ = ['BarclaysBackend'] + + +class BarclaysBackend(BaseBackend, ICapBank): + NAME = 'barclays' + MAINTAINER = u'Romain Bignon' + EMAIL = 'romain@weboob.org' + VERSION = '0.e' + DESCRIPTION = u'Barclays French bank website' + LICENSE = 'AGPLv3+' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password'), + ValueBackendPassword('secret', label='Secret word')) + BROWSER = Barclays + + def create_default_browser(self): + return self.create_browser(self.config['secret'].get(), + self.config['login'].get(), + self.config['password'].get()) + + def iter_accounts(self): + with self.browser: + return self.browser.get_accounts_list() + + def get_account(self, _id): + with self.browser: + account = self.browser.get_account(_id) + + if account: + return account + else: + raise AccountNotFound() + + def iter_history(self, account): + with self.browser: + return self.browser.get_history(account) + + def iter_coming(self, account): + with self.browser: + return self.browser.get_coming_operations(account) diff --git a/modules/barclays/browser.py b/modules/barclays/browser.py new file mode 100644 index 00000000..ae92fbd1 --- /dev/null +++ b/modules/barclays/browser.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword + +from .pages import LoginPage, Login2Page, IndexPage, AccountsPage, TransactionsPage, CardPage, ValuationPage + + +__all__ = ['Barclays'] + + +class Barclays(BaseBrowser): + PROTOCOL = 'https' + DOMAIN = 'www.barclays.fr' + PAGES = {'https?://.*.barclays.fr/\d-index.html': IndexPage, + 'https://.*.barclays.fr/barclaysnetV2/logininstit.do.*': LoginPage, + 'https://.*.barclays.fr/barclaysnetV2/loginSecurite.do.*': Login2Page, + 'https://.*.barclays.fr/barclaysnetV2/tbord.do.*': AccountsPage, + 'https://.*.barclays.fr/barclaysnetV2/releve.do.*': TransactionsPage, + 'https://.*.barclays.fr/barclaysnetV2/cartes.do.*': CardPage, + 'https://.*.barclays.fr/barclaysnetV2/valuationViewBank.do.*': ValuationPage, + } + + def __init__(self, secret, *args, **kwargs): + self.secret = secret + + BaseBrowser.__init__(self, *args, **kwargs) + + def is_logged(self): + return self.page is not None and not self.is_on_page((LoginPage, IndexPage, LoginPage)) + + def home(self): + if self.is_logged(): + self.location('tbord.do') + else: + self.login() + + def login(self): + """ + Attempt to log in. + Note: this method does nothing if we are already logged in. + """ + assert isinstance(self.username, basestring) + assert isinstance(self.password, basestring) + + if self.is_logged(): + return + + if not self.is_on_page(LoginPage): + self.location('https://b-net.barclays.fr/barclaysnetV2/logininstit.do?lang=fr&nodoctype=0', no_login=True) + + self.page.login(self.username, self.password) + + if not self.page.has_redirect(): + raise BrowserIncorrectPassword() + + self.location('loginSecurite.do', no_login=True) + + self.page.login(self.secret) + + if not self.is_logged(): + raise BrowserIncorrectPassword() + + def get_accounts_list(self): + if not self.is_on_page(AccountsPage): + self.location('tbord.do') + return self.page.get_list() + + def get_account(self, id): + assert isinstance(id, basestring) + + l = self.get_accounts_list() + for a in l: + if a.id == id: + return a + + return None + + def get_history(self, account): + self.location(account._link) + + assert self.is_on_page((TransactionsPage, ValuationPage)) + + return self.page.get_history() + + def get_coming_operations(self, account): + for card in account._card_links: + self.location(card) + + assert self.is_on_page(CardPage) + + for tr in self.page.get_history(): + yield tr diff --git a/modules/barclays/favicon.png b/modules/barclays/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..fe46a7a214c55d3e71ace399b8d4b0c55b14ef93 GIT binary patch literal 1642 zcmV-w29^1VP)BFexO-*r)&i02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00q)XL_t(|+U=X~PZM_>$6wb% zd!-b?$rQ|{GL|v7Y(ix-BvWt?%#504$&$?{J~I9pS03z7FiV!jWJ|VWPe^96JbvjWt#39N=L5fG~wWYm1_)<%0X?w?k{lPmSA#J~R-|zeL`*WX@PM$Fgsu&Kc z9P0#BI{`JTbzz@AL)+tXF z|Mmb4(NMwPPIF*|8ePH4&wdrqMKIxtzr1WYvUV0d#U zJ0D%8CJ{q&xx|5cdU76mDK^%>7y@k11GY56!CfHNnp>yG;gnhdc|cf)Xx#idxfb@c zR{#N;4w0C(wVQn4lPjPWn2kV7qc!u%8);W$C~*6 z*ayPiEys5W-rNc^k^GvlECHW%=XXj@$AV_a@KQaz(E{g(U}B-Fef$(+^WZj-4;X-4 z1IPQI)&}F0Bfqzu@ag;MQ1&Eja>oXI__jTVPodG9 zUz4&3*wYS8Uh6k(Zh+5vp!>B2k(!j4yB1sFbU?GONE7hM!s)COwNB`0g?Pd|0mb?X zV?kpisqi|Ft4Iy-$}4j`xrkVm0!9LdB#O;QOwm2}l}iTn?8SQWUfk=as4hEQAaKG+mUAm0||-$-)B7@&T9ch;B|LAR5mf0hWydKV2;! zU!bZgs;cJvPJ33kIhvCIO@~_#%1he){ry>=O=%{5z+zne7v>YCfAHSkUV3|bQB^gI zgivG!1ZaBU7+PwkuvilkNzs?IOaO@9daj6&OWj_4%r`Rp!IQakhuJa#4`)j^BIFW6 z${bQATnzFOFrUcQ0>9tS;lqbZk_1IjP!t6{4wJJs5CAYfo$k2>gFyhgy1Gir?w6*& zH^+;AHdv4VO^2cTJU@7fgcPJ@dT49mCsOLZw4NC<5yFdl1abKe{P1-@Ubk>IwsdNe zqN`ghBuUD+x1uNngF*a$KSz!nK~WS81FnskdrWR>MCRDu%r0z7%8H^C-0;#O<=U8d zf^3F>Kp>FwUa5T?J)v;+hB-l)YKVOEOWGe;J$x!hAG!}A6A>m52rQfO1_A-5HB2Ag zQb*{NYJC7Ag*vx*+x4#P3bHVTTGdMU{ z)OEtTNLb7yJApuezP`TaVfwp^#SKE(NFG*sY|7j{Qzo}}Umstc zvo;rG3jrt37@WNUzx|W;3x^IJqPx30@49C(xIB{H_@xo^+2-k=&*OV_pDoeImO)9M z3{NM5eEG$3E?yeMU7J0~6q^^*V?z&M_`mFH6dK5Qo}Pzb;EH{@ge{{8nhw&&E^gik za(d9(DBHjlF_N>lBXhPWkrXB~J`E$2=?N6OAeG{KgRM=1K#xmQbXI2o*GvQK#>5)F oJ+uDa;5q^81gsOVPCzx|e=k)%>wrG}jsO4v07*qoM6N<$g8$I;6951J literal 0 HcmV?d00001 diff --git a/modules/barclays/pages.py b/modules/barclays/pages.py new file mode 100644 index 00000000..02ebe2b8 --- /dev/null +++ b/modules/barclays/pages.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from decimal import Decimal +import re + +from weboob.tools.browser import BasePage +from weboob.capabilities.bank import Account +from weboob.tools.capabilities.bank.transactions import FrenchTransaction + + +__all__ = ['LoginPage', 'Login2Page', 'IndexPage', 'AccountsPage', 'TransactionsPage', + 'CardPage', 'ValuationPage'] + + +class LoginPage(BasePage): + def login(self, login, passwd): + self.browser.select_form(name='frmLogin') + self.browser['username'] = login.encode(self.browser.ENCODING) + self.browser['password'] = passwd.encode(self.browser.ENCODING) + self.browser.submit(nologin=True) + + def has_redirect(self): + if len(self.document.getroot().xpath('//form')) > 0: + return False + else: + return True + +class Login2Page(BasePage): + def login(self, secret): + label = self.document.xpath('//span[@class="PF_LABEL"]')[0].text.strip() + letters = '' + for n in re.findall('(\d+)', label): + letters += secret[int(n) - 1] + + self.browser.select_form(name='frmControl') + self.browser['word'] = letters + self.browser.submit(name='valider', nologin=True) + +class IndexPage(BasePage): + pass + +class AccountsPage(BasePage): + ACCOUNT_TYPES = {u'Epargne': Account.TYPE_SAVINGS, + u'Liquidités': Account.TYPE_CHECKING, + } + + def get_list(self): + accounts = [] + + for block in self.document.xpath('//div[@class="pave"]/div'): + head_type = block.xpath('./div/span[@class="accGroupLabel"]')[0].text.strip() + account_type = self.ACCOUNT_TYPES.get(head_type, Account.TYPE_UNKNOWN) + for tr in block.cssselect('ul li.tbord_account'): + id = tr.attrib.get('id', '') + if id.find('contratId') != 0: + self.logger.warning('Unable to parse contract ID: %r' % id) + continue + id = id[id.find('contratId')+len('contratId'):] + + link = tr.cssselect('span.accountLabel a')[0] + balance = Decimal(FrenchTransaction.clean_amount(tr.cssselect('span.accountTotal')[0].text)) + + if id.endswith('CRT'): + account = accounts[-1] + account._card_links.append(link.attrib['href']) + if not account.coming: + account.coming = Decimal('0.0') + account.coming += balance + continue + + account = Account() + account.id = id + account.label = unicode(link.text.strip()) + account.type = account_type + account.balance = balance + account.currency = account.get_currency(tr.cssselect('span.accountDev')[0].text) + account._link = link.attrib['href'] + account._card_links = [] + accounts.append(account) + + return accounts + + +class Transaction(FrenchTransaction): + PATTERNS = [(re.compile('^RET DAB (?P.*?) RETRAIT DU (?P
\d{2})(?P\d{2})(?P\d{2}).*'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('^RET DAB (?P.*?) CARTE ?:.*'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('^(?P.*) RETRAIT DU (?P
\d{2})(?P\d{2})(?P\d{2}) .*'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('(\w+) (?P
\d{2})(?P\d{2})(?P\d{2}) CB[:\*][^ ]+ (?P.*)'), + FrenchTransaction.TYPE_CARD), + (re.compile('^(?PVIR(EMEN)?T? (SEPA)?(RECU|FAVEUR)?)( /FRM)?(?P.*)'), + FrenchTransaction.TYPE_TRANSFER), + (re.compile('^PRLV (?P.*) (REF \w+)?$'),FrenchTransaction.TYPE_ORDER), + (re.compile('^CHEQUE.*? (REF \w+)?$'), FrenchTransaction.TYPE_CHECK), + (re.compile('^(AGIOS /|FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK), + (re.compile('^(CONVENTION \d+ )?COTIS(ATION)? (?P.*)'), + FrenchTransaction.TYPE_BANK), + (re.compile('^REMISE (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), + (re.compile('^(?P.*)( \d+)? QUITTANCE .*'), + FrenchTransaction.TYPE_ORDER), + (re.compile('^.* LE (?P
\d{2})/(?P\d{2})/(?P\d{2})$'), + FrenchTransaction.TYPE_UNKNOWN), + ] + + +class TransactionsPage(BasePage): + def get_history(self): + for tr in self.document.xpath('//table[@id="operation"]/tbody/tr'): + tds = tr.findall('td') + + if len(tds) < 5: + continue + + t = Transaction(tds[-1].findall('img')[-1].attrib.get('id', '')) + + date = u''.join([txt.strip() for txt in tds[0].itertext()]) + raw = u' '.join([txt.strip() for txt in tds[1].itertext()]) + debit = u''.join([txt.strip() for txt in tds[-3].itertext()]) + credit = u''.join([txt.strip() for txt in tds[-2].itertext()]) + t.parse(date, re.sub(r'[ ]+', ' ', raw)) + t.set_amount(credit, debit) + + if t.raw.startswith('ACHAT CARTE -DEBIT DIFFERE'): + continue + + yield t + + +class CardPage(BasePage): + def get_history(self): + for tr in self.document.xpath('//table[@class="report"]/tbody/tr'): + tds = tr.findall('td') + + if len(tds) != 3: + #header + continue + + t = Transaction(0) + date = u''.join([txt.strip() for txt in tds[0].itertext()]) + raw = u' '.join([txt.strip() for txt in tds[1].itertext()]) + amount = u''.join([txt.strip() for txt in tds[-1].itertext()]) + t.parse(date, re.sub(r'[ ]+', ' ', raw)) + t.label = tds[1].find('span').text.strip() + t.type = t.TYPE_CARD + t.set_amount(amount) + yield t + +class ValuationPage(BasePage): + def get_history(self): + return iter([]) diff --git a/modules/barclays/test.py b/modules/barclays/test.py new file mode 100644 index 00000000..ab919703 --- /dev/null +++ b/modules/barclays/test.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Romain Bignon +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.test import BackendTest + +class BarclaysTest(BackendTest): + BACKEND = 'barclays' + + def test_banquepop(self): + l = list(self.backend.iter_accounts()) + if len(l) > 0: + a = l[0] + list(self.backend.iter_history(a))