From fd9d06944223645c975ec9d97e64abe8ae057fee Mon Sep 17 00:00:00 2001 From: Romain Bignon Date: Sat, 9 Feb 2013 18:05:22 +0100 Subject: [PATCH] add module creditdunord --- modules/creditdunord/__init__.py | 23 +++++ modules/creditdunord/backend.py | 70 +++++++++++++ modules/creditdunord/browser.py | 123 +++++++++++++++++++++++ modules/creditdunord/favicon.png | Bin 0 -> 2103 bytes modules/creditdunord/pages.py | 163 +++++++++++++++++++++++++++++++ modules/creditdunord/test.py | 31 ++++++ 6 files changed, 410 insertions(+) create mode 100644 modules/creditdunord/__init__.py create mode 100644 modules/creditdunord/backend.py create mode 100644 modules/creditdunord/browser.py create mode 100644 modules/creditdunord/favicon.png create mode 100644 modules/creditdunord/pages.py create mode 100644 modules/creditdunord/test.py diff --git a/modules/creditdunord/__init__.py b/modules/creditdunord/__init__.py new file mode 100644 index 00000000..dabc8016 --- /dev/null +++ b/modules/creditdunord/__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 CreditDuNordBackend + +__all__ = ['CreditDuNordBackend'] diff --git a/modules/creditdunord/backend.py b/modules/creditdunord/backend.py new file mode 100644 index 00000000..cba160ce --- /dev/null +++ b/modules/creditdunord/backend.py @@ -0,0 +1,70 @@ +# -*- 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 __future__ import with_statement + +from weboob.capabilities.bank import ICapBank, AccountNotFound +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import CreditDuNordBrowser + + +__all__ = ['CreditDuNordBackend'] + + +class CreditDuNordBackend(BaseBackend, ICapBank): + NAME = 'creditdunord' + MAINTAINER = u'Romain Bignon' + EMAIL = 'romain@weboob.org' + VERSION = '0.f' + DESCRIPTION = u'Crédit du Nord French bank website' + LICENSE = 'AGPLv3+' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password of account')) + BROWSER = CreditDuNordBrowser + + def create_default_browser(self): + return self.create_browser(self.config['login'].get(), self.config['password'].get()) + + def iter_accounts(self): + with self.browser: + for account in self.browser.get_accounts_list(): + yield account + + 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: + transactions = list(self.browser.get_history(account)) + transactions.sort(key=lambda tr: tr.rdate, reverse=True) + return [tr for tr in transactions if not tr._is_coming] + + def iter_coming(self, account): + with self.browser: + transactions = list(self.browser.get_card_operations(account)) + transactions.sort(key=lambda tr: tr.rdate, reverse=True) + return [tr for tr in transactions if tr._is_coming] diff --git a/modules/creditdunord/browser.py b/modules/creditdunord/browser.py new file mode 100644 index 00000000..596949cb --- /dev/null +++ b/modules/creditdunord/browser.py @@ -0,0 +1,123 @@ +# -*- 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 . + + +import urllib + +from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword + +from .pages import LoginPage, AccountsPage, TransactionsPage + + +__all__ = ['CreditDuNordBrowser'] + + +class CreditDuNordBrowser(BaseBrowser): + PROTOCOL = 'https' + DOMAIN = 'www.credit-du-nord.fr' + #CERTHASH = 'b2f8a8a7a03c54d7bb918f10eb4e141c3fb51bebf0eb8371aefb33a997efc600' + ENCODING = 'UTF-8' + PAGES = {'https://www.credit-du-nord.fr/?': LoginPage, + 'https://www.credit-du-nord.fr/vos-comptes/particuliers(\?.*)?': AccountsPage, + 'https://www.credit-du-nord.fr/vos-comptes/.*/transac/.*': TransactionsPage, + } + + def is_logged(self): + return self.page is not None and not self.is_on_page(LoginPage) + + def home(self): + if self.is_logged(): + self.location('https://www.credit-du-nord.fr/vos-comptes/particuliers') + else: + self.login() + return + return self.location('https://www.credit-du-nord.fr/vos-comptes/particuliers') + + def login(self): + assert isinstance(self.username, basestring) + assert isinstance(self.password, basestring) + + # not necessary (and very slow) + #self.location('https://www.credit-du-nord.fr/', no_login=True) + + data = {'bank': 'credit-du-nord', + 'pagecible': 'vos-comptes', + 'password': self.password.encode(self.ENCODING), + 'pwAuth': 'Authentification+mot+de+passe', + 'username': self.username.encode(self.ENCODING), + } + + self.location('https://www.credit-du-nord.fr/saga/authentification', urllib.urlencode(data), no_login=True) + + if not self.is_logged(): + raise BrowserIncorrectPassword() + + def get_accounts_list(self): + if not self.is_on_page(AccountsPage): + self.location('https://www.credit-du-nord.fr/vos-comptes/particuliers') + 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 iter_transactions(self, link, link_id, execution, is_coming=None): + event = 'clicDetailCompte' + while 1: + data = {'_eventId': event, + '_ipc_eventValue': '', + '_ipc_fireEvent': '', + 'deviseAffichee': 'DEVISE', + 'execution': execution, + 'idCompteClique': link_id, + } + self.location(link, urllib.urlencode(data)) + + assert self.is_on_page(TransactionsPage) + + self.page.is_coming = is_coming + + for tr in self.page.get_history(): + yield tr + + is_last = self.page.is_last() + if is_last: + return + + event = 'clicChangerPageSuivant' + execution = self.page.get_execution() + is_coming = self.page.is_coming + + def get_history(self, account): + for tr in self.iter_transactions(account._link, account._link_id, account._execution): + yield tr + + for tr in self.get_card_operations(account): + yield tr + + def get_card_operations(self, account): + for link_id in account._card_ids: + for tr in self.iter_transactions(account._link, link_id, account._execution, True): + yield tr diff --git a/modules/creditdunord/favicon.png b/modules/creditdunord/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..28bbb53a3cc129357836ec426d87759b19bab2d7 GIT binary patch literal 2103 zcmV-72*~$|P)xl{2gFH4 zK~#9!?VEjURrMLiKfl`-TC|1JTeT%R2x}^eKy%PEaY|%}WR5p8wa)sokWE;_HWy7y z_@gh|vMiHjX3LD5A!N>EGPkg$@`uJ+T_kYCUI}l(u45oWp!9I*3w^)m?2r3gdV71$ zeSc|Jo}_7Rf4_6i@A*D&-{(2E%C>E0v*ZJ)s)&jlXNwend9KzX$$paQrVC{bQ*Yc7?)mIJaVi+~5^x%q&R zOXYJB0_(#-znakVos_R@n{fb?19q7Ay_EEw>e^ z=mqM5>Tq0uWx%Ka^#54~rOy8#%JY{4=M%PiG*H~u9Kt9YRp5~r>jl6=DV4!wl&@=p zt~FT+fq2^9!n9NZx3wtOWWxi#8P9M=d9^0n^tU8k3HGc(HTd*US+!r)Tc~S`Sv_(! z5t;%(GL}~jJ{_<29B>ao<;NtXn&VM^Isp(!D6uy9c8jIw2)oP|Mp*)!boC^M>XA_Z zq`ZZ!5=};8Szk5ynj}~30IY}-ZUd!&X`B8#5-FY8@PK#{?L}at@@lQN>GwqdSV)6! z2XF%TYLt6+1K&uLFl8EmX7PZ$82d_rKZ#rWlvmS%4qy*(KQIp58Rhy*(pvixB}<+R zyMJ>W%7-RJ8|1p9z&=r`PUZ-)40W?@`t|9^1RskNWCg4MiV2R)Z;BJR%YX?yF|DYu zSbr6;G{LhhNx&;dfbMHaFIxaee1>F$WactpTf((H`H&m|I%UR?o@U7Ia;A%(TAwb> zOpW}U7lrdBIav-=HV~%^Co6|6L%-i~;4m1#-#f;svqKy_+{@ngE^+X%`_M_*wvDQ4 zn*x9+{xq;ftXU)jjXW7=#^m66!Zq@c6^G;PFg7Vjy7iqn~#2yEp$k2>`d4 z`v^D(42H#WXr7;oFBFUdj{++!L+=P*7!TN7=o#gmc9U;Ef1K6zK6);XPPsu|p5nJn z)jYMSB6iZTO@Fgx=*y%YUliq!16{y>vIv3=ya(u(q3^W}y+76k|2%Ys&D)L;2uwYq zQi|U+R#UDO#Fup~Lx0{f^aYloE8wrdyOQ9PWa2?#dREwFJ@WTdlpW!A<`wYW#}_2d1T8~vwhaA;mZ4YMre6W~0xttQfzM^zTLM(40FxAr z0G8XP|A9mjU8MZE^`h(A`mdL#PK1Pahq(6+NuJGOL4)Arw>*ZWof7I5;8~z0mDu_0 zgnP79EiBFq6SPf#a~N5AGRoF*36mlR4p9o!E3eiX25`dC+duBQrj-^X9s)_o+$d`U zz-q}UPHfNuUQgxW^Gn@rYhD@48~{&mu0RfNxoqhHE=UOdlHlvINVy$oPvso?(h}FT zf1Mso4*&|dq1Z$9qGIYd-om@PYj|VlQh*zxP~o0X17n;1kAbr;`(8-pZ1o!}T-RDZ z=t(UHxb+PY7PMVrzdqt`PXGra#%4C~fv48jWEpzG$?$9XJf3s`py%=^yZ-WJ=0ZSv zh!KYjZv(%cG_{$iCWi!^@NO@ueq+@=^ZEO0%gM`|T3~G3W_!!$`1{6F^MLT)VQ!I! z(i`)sT38&4yA9GbMZWnx0sQ2)$~l}pKV=5G6*wxDp;)TJ3ZTqW>ua?P-L)Fz=PA7N zC-(wQ%8!p*Y;W#l&%W;T4nzirEe3~2xzIDp#~r;WuhvL#L>;R0mNZpcEJGi-(l^H1 zXFlW8W0UH1C&9PJ_X4$YT>)?`G$%|V3@3W$BYk|o@gyC``ZGHblKSrntL?)A#&+Ov zusjv8Z&wXV)*iU#ini$=1zuEMZKqg1R6^GYFNix23=Re70ssCYzIi~pM`G_p+_w`@ zmZ85rX@co3Y+Z^T87ejQN8DhV$m-Yjf=^Y3#9VI{A)H0<~nu9ZVnB{UEfa?HU h2Vi!Kn(hBS|37YN$A1*fXJ!BZ002ovPDHLkV1gTz<}?5R literal 0 HcmV?d00001 diff --git a/modules/creditdunord/pages.py b/modules/creditdunord/pages.py new file mode 100644 index 00000000..217d2393 --- /dev/null +++ b/modules/creditdunord/pages.py @@ -0,0 +1,163 @@ +# -*- 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 cStringIO import StringIO + +from weboob.tools.browser import BasePage, BrokenPageError +from weboob.tools.json import json +from weboob.capabilities.bank import Account +from weboob.tools.capabilities.bank.transactions import FrenchTransaction + + +__all__ = ['LoginPage', 'AccountsPage', 'TransactionsPage'] + + +class LoginPage(BasePage): + pass + +class CDNBasePage(BasePage): + def get_from_js(self, pattern, end): + """ + find a pattern in any javascript text + """ + for script in self.document.xpath('//script'): + txt = script.text + if txt is None: + continue + + start = txt.find(pattern) + if start < 0: + continue + + txt = txt[start+len(pattern):start+txt[start+len(pattern):].find(end)+len(pattern)] + return txt + + def get_execution(self): + return self.get_from_js("name: 'execution', value: '", "'") + +class AccountsPage(CDNBasePage): + COL_ID = 4 + COL_LABEL = 5 + COL_BALANCE = -1 + + def get_history_link(self): + return self.parser.strip(self.get_from_js(",url: Ext.util.Format.htmlDecode('", "'")) + + def get_list(self): + accounts = [] + + txt = self.get_from_js('_data = new Array(', ');') + + if txt is None: + raise BrokenPageError('Unable to find accounts list in scripts') + + data = json.loads('[%s]' % txt.replace("'", '"')) + + for line in data: + a = Account() + a.id = line[self.COL_ID].replace(' ','') + a.label = self.parser.tocleanstring(self.parser.parse(StringIO(line[self.COL_LABEL])).xpath('//div[@class="libelleCompteTDB"]')[0]) + a.balance = Decimal(FrenchTransaction.clean_amount(line[self.COL_BALANCE])) + a._link = self.get_history_link() + a._execution = self.get_execution() + a._link_id = line[self.COL_ID] + + if a.id.endswith('_CarteVisaPremier'): + accounts[0]._card_ids.append(a._link_id) + if not accounts[0].coming: + accounts[0].coming = Decimal('0.0') + accounts[0].coming += a.balance + continue + + a._card_ids = [] + accounts.append(a) + + return iter(accounts) + +class Transaction(FrenchTransaction): + PATTERNS = [(re.compile(r'^(?PRET DAB \w+ .*?) LE (?P
\d{2})(?P\d{2})$'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile(r'^VIR(EMENT)?\.?(DE)? (?P.*)'), + FrenchTransaction.TYPE_TRANSFER), + (re.compile(r'^PRLV (DE )?(?P.*)'), FrenchTransaction.TYPE_ORDER), + (re.compile(r'^CB (?P.*) LE (?P
\d{2})\.?(?P\d{2})$'), + FrenchTransaction.TYPE_CARD), + (re.compile(r'^CHEQUE.*'), FrenchTransaction.TYPE_CHECK), + (re.compile(r'^(CONVENTION \d+ )?COTISATION (?P.*)'), + FrenchTransaction.TYPE_BANK), + (re.compile(r'^REM(ISE)?\.?( CHQ\.)? .*'), FrenchTransaction.TYPE_DEPOSIT), + (re.compile(r'^(?P.*?)( \d{2}.*)? LE (?P
\d{2})\.?(?P\d{2})$'), + FrenchTransaction.TYPE_CARD), + ] + + +class TransactionsPage(CDNBasePage): + COL_ID = 0 + COL_DATE = 1 + COL_DEBIT_DATE = 2 + COL_LABEL = 3 + COL_VALUE = -1 + + is_coming = None + + def is_last(self): + for script in self.document.xpath('//script'): + txt = script.text + if txt is None: + continue + + if txt.find('clicChangerPageSuivant') >= 0: + return False + + return True + + def get_history(self): + txt = self.get_from_js('ListeMvts_data = new Array(', ');') + + if txt is None: + raise BrokenPageError('Unable to find transactions list in scripts') + + data = json.loads('[%s]' % txt.replace('"', '\\"').replace("'", '"')) + + for line in data: + t = Transaction(line[self.COL_ID]) + + if self.is_coming is not None: + t.type = t.TYPE_CARD + date = self.parser.strip(line[self.COL_DEBIT_DATE]) + else: + date = self.parser.strip(line[self.COL_DATE]) + raw = self.parser.strip(line[self.COL_LABEL]) + + t.parse(date, raw) + t.set_amount(line[self.COL_VALUE]) + + if self.is_coming is True and raw.startswith('TOTAL DES') and t.amount > 0: + # ignore card credit and next transactions are already debited + self.is_coming = False + continue + if self.is_coming is None and raw.startswith('ACHATS CARTE'): + # Ignore card debit + continue + + t._is_coming = bool(self.is_coming) + yield t diff --git a/modules/creditdunord/test.py b/modules/creditdunord/test.py new file mode 100644 index 00000000..4cf04dbd --- /dev/null +++ b/modules/creditdunord/test.py @@ -0,0 +1,31 @@ +# -*- 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 CreditDuNordTest(BackendTest): + BACKEND = 'creditdunord' + + def test_creditdunord(self): + l = list(self.backend.iter_accounts()) + + a = l[0] + list(self.backend.iter_history(a)) + list(self.backend.iter_coming(a))