From 2246f893bcc059c02acf0d526654962d071edb7e Mon Sep 17 00:00:00 2001 From: Romain Bignon Date: Fri, 28 Dec 2012 17:48:52 +0100 Subject: [PATCH] add module for Credit Mutuel Sud Ouest (cmso) --- modules/cmso/__init__.py | 23 +++++++++ modules/cmso/backend.py | 61 ++++++++++++++++++++++ modules/cmso/browser.py | 91 ++++++++++++++++++++++++++++++++ modules/cmso/favicon.png | Bin 0 -> 2336 bytes modules/cmso/pages.py | 109 +++++++++++++++++++++++++++++++++++++++ modules/cmso/test.py | 30 +++++++++++ 6 files changed, 314 insertions(+) create mode 100644 modules/cmso/__init__.py create mode 100644 modules/cmso/backend.py create mode 100644 modules/cmso/browser.py create mode 100644 modules/cmso/favicon.png create mode 100644 modules/cmso/pages.py create mode 100644 modules/cmso/test.py diff --git a/modules/cmso/__init__.py b/modules/cmso/__init__.py new file mode 100644 index 00000000..aaa435e0 --- /dev/null +++ b/modules/cmso/__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 CmsoBackend + +__all__ = ['CmsoBackend'] diff --git a/modules/cmso/backend.py b/modules/cmso/backend.py new file mode 100644 index 00000000..6df83de7 --- /dev/null +++ b/modules/cmso/backend.py @@ -0,0 +1,61 @@ +# -*- 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 Cmso + + +__all__ = ['CmsoBackend'] + + +class CmsoBackend(BaseBackend, ICapBank): + NAME = 'cmso' + MAINTAINER = u'Romain Bignon' + EMAIL = 'romain@weboob.org' + VERSION = '0.e' + DESCRIPTION = u'Credit Mutuel Sud Ouest French bank website' + LICENSE = 'AGPLv3+' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password')) + BROWSER = Cmso + + def create_default_browser(self): + return self.create_browser(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) diff --git a/modules/cmso/browser.py b/modules/cmso/browser.py new file mode 100644 index 00000000..b64bd555 --- /dev/null +++ b/modules/cmso/browser.py @@ -0,0 +1,91 @@ +# -*- 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, AccountsPage, TransactionsPage + + +__all__ = ['Cmso'] + + +class Cmso(BaseBrowser): + PROTOCOL = 'https' + DOMAIN = 'www.cmso.com' + PAGES = {'https://www.cmso.com/domimobile/m.jsp\?a=signin.*': LoginPage, + 'https://www.cmso.com/domimobile/m.jsp\?a=sommaire.*': AccountsPage, + 'https://www.cmso.com/domimobile/m.jsp\?a=solde.*': TransactionsPage, + 'https://www.cmso.com/domimobile/m.jsp\?rels=.*': 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.cmso.com/domimobile/m.jsp?a=sommaire') + 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://www.cmso.com/domimobile/m.jsp?a=signin&b=sommaire', no_login=True) + + self.page.login(self.username, self.password) + + if not self.is_logged(): + raise BrowserIncorrectPassword() + + def get_accounts_list(self): + if not self.is_on_page(AccountsPage): + self.location('https://www.cmso.com/domimobile/m.jsp?a=sommaire') + 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): + link = account._link + + while link is not None: + self.location(link) + assert self.is_on_page(TransactionsPage) + + for tr in self.page.get_history(): + yield tr + + link = self.page.get_next_link() diff --git a/modules/cmso/favicon.png b/modules/cmso/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..4aa8c947abba4cc603adb7833c5c76883afb37eb GIT binary patch literal 2336 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. + + +import datetime +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', 'AccountsPage', 'TransactionsPage'] + + +class LoginPage(BasePage): + def login(self, login, passwd): + self.browser.select_form(name='formIdentification') + self.browser['noPersonne'] = login.encode(self.browser.ENCODING) + self.browser['motDePasse'] = passwd.encode(self.browser.ENCODING) + self.browser.submit(nologin=True) + +class AccountsPage(BasePage): + def get_list(self): + for li in self.document.xpath('//div[@class="affichMontant"]/ul/li/a'): + account = Account() + account.id = li.attrib['accesskey'] + account.label = unicode(li.cssselect('div.row-lib u')[0].text.strip()) + account.balance = Decimal(li.cssselect('p.row-right')[0].text.strip().replace(' ', '').replace(',', '')) + account._link = li.attrib['href'] + yield account + + +class Transaction(FrenchTransaction): + PATTERNS = [(re.compile('^RET DAB (?P
\d{2})/(?P\d{2})/(?P\d{2}) (?P.*)'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('CARTE (?P
\d{2})/(?P\d{2}) (?P.*)'), + FrenchTransaction.TYPE_CARD), + (re.compile('^(?PVIR(EMEN)?T? (SEPA)?(RECU|FAVEUR)?)( /FRM)?(?P.*)'), + FrenchTransaction.TYPE_TRANSFER), + (re.compile('^PRLV (?P.*)( \d+)?$'), FrenchTransaction.TYPE_ORDER), + (re.compile('^(CHQ|CHEQUE) .*$'), FrenchTransaction.TYPE_CHECK), + (re.compile('^(AGIOS /|FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK), + (re.compile('^(CONVENTION \d+ |F )?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): + months = [u'janvier', u'février', u'mars', u'avril', u'mai', u'juin', u'juillet', u'août', u'septembre', u'octobre', u'novembre', u'décembre'] + + def get_next_link(self): + a = self.document.getroot().cssselect('div#nav-sub p.row-right a') + if len(a) == 0: + return None + + return a[0].attrib['href'] + + def get_history(self): + for div in self.document.xpath('//ol[@class="affichMontant"]/li/div'): + t = Transaction(0) + raw = div.xpath('.//div[@class="row-lib"]')[0].text + date = div.xpath('.//span')[0].text.strip() + m = re.match('(\d+) ([^ ]+)( \d+)?$', date) + if m: + dd = int(m.group(1)) + mm = self.months.index(m.group(2)) + 1 + if m.group(3) is not None: + yy = int(m.group(3)) + else: + + d = datetime.date.today() + try: + d = d.replace(month=mm, day=dd) + except ValueError: + d = d.replace(year=d.year-1, month=mm, day=dd) + + yy = d.year + if d > datetime.date.today(): + yy -= 1 + + date = datetime.date(yy, mm, dd) + + t.parse(date, re.sub(r'[ ]+', ' ', raw)) + t.amount = Decimal(div.xpath('.//span')[-1].text.strip().replace(' ', '').replace(',', '')) + + yield t diff --git a/modules/cmso/test.py b/modules/cmso/test.py new file mode 100644 index 00000000..d13661b9 --- /dev/null +++ b/modules/cmso/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 CmsoTest(BackendTest): + BACKEND = 'cmso' + + def test_cmso(self): + l = list(self.backend.iter_accounts()) + if len(l) > 0: + a = l[0] + list(self.backend.iter_history(a))