diff --git a/modules/fortuneo/__init__.py b/modules/fortuneo/__init__.py new file mode 100644 index 00000000..d0c8b64a --- /dev/null +++ b/modules/fortuneo/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Gilles-Alexandre Quenot +# +# 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 FortuneoBackend + +__all__ = ['SocieteGeneraleBackend'] diff --git a/modules/fortuneo/backend.py b/modules/fortuneo/backend.py new file mode 100644 index 00000000..ddb85799 --- /dev/null +++ b/modules/fortuneo/backend.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Gilles-Alexandre Quenot +# +# 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 . + + +# python2.5 compatibility +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 Fortuneo + + +__all__ = ['FortuneoBackend'] + + +class FortuneoBackend(BaseBackend, ICapBank): + NAME = 'fortuneo' + MAINTAINER = 'Gilles-Alexandre Quenot' + EMAIL = 'gilles.quenot@gmail.com' + VERSION = '0.c' + LICENSE = 'AGPLv3+' + DESCRIPTION = u'Fortuneo French bank website' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password')) + BROWSER = Fortuneo + + def create_default_browser(self): + return self.create_browser(self.config['login'].get(), + self.config['password'].get()) + + def iter_accounts(self): + for account in self.browser.get_accounts_list(): + yield account + + def get_account(self, _id): + #pass + #if not _id.isdigit(): + # raise AccountNotFound() + with self.browser: + account = self.browser.get_account(_id) + if account: + return account + else: + raise AccountNotFound() + + def iter_history(self, account): + pass + #with self.browser: + # for tr in self.browser.iter_history(account._link_id): + # if not tr._coming: + # yield tr + + def iter_coming(self, account): + with self.browser: + for tr in self.browser.iter_history(account._link_id): + if tr._coming: + yield tr diff --git a/modules/fortuneo/browser.py b/modules/fortuneo/browser.py new file mode 100644 index 00000000..4f427953 --- /dev/null +++ b/modules/fortuneo/browser.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Gilles-Alexandre Quenot +# +# 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.accounts_list import AccountsList, AccountHistory +from .pages.login import LoginPage, BadLoginPage + + +__all__ = ['Fortuneo'] + + +class Fortuneo(BaseBrowser): + DOMAIN_LOGIN = 'www.fortuneo.fr' + DOMAIN = 'www.fortuneo.fr' + PROTOCOL = 'https' + ENCODING = None # refer to the HTML encoding + PAGES = { + '.*identification.jsp.*': LoginPage, + #'https://www.fortuneo.fr/fr/identification.jsp': BadLoginPage, + '.*/prive/default.jsp.*': AccountsList, + '.*/prive/mes-comptes/livret/consulter-situation/consulter-solde.jsp.*': AccountHistory, + } + + def __init__(self, *args, **kwargs): + BaseBrowser.__init__(self, *args, **kwargs) + + def home(self): + self.location('https://' + self.DOMAIN_LOGIN + '/fr/identification.jsp') + #self.location('https://' + self.DOMAIN_LOGIN + '/fr/prive/default.jsp?ANav=1') + #self.location('https://' + self.DOMAIN_LOGIN + '/fr/prive/mes-comptes/synthese-tous-comptes.jsp') + + def is_logged(self): + return not self.is_on_page(LoginPage) + + def login(self): + assert isinstance(self.username, basestring) + assert isinstance(self.password, basestring) + #assert self.password.isdigit() + + if not self.is_on_page(LoginPage): + self.location('https://' + self.DOMAIN_LOGIN + '/fr/identification.jsp') + + self.page.login(self.username, self.password) + + if self.is_on_page(LoginPage) or \ + self.is_on_page(BadLoginPage): + raise BrowserIncorrectPassword() + + def get_accounts_list(self): + if not self.is_on_page(AccountsList): + self.location('/fr/prive/mes-comptes/synthese-globale/synthese-tous-comptes.jsp') + #self.location('') + + return self.page.get_list() + + def get_account(self, id): + assert isinstance(id, basestring) + + #if not self.is_on_page(AccountsList): + # self.location('/fr/prive/default.jsp?ANav=1') + + l = self.page.get_list() + for a in l: + if a.id == id: + return a + + return None + + def iter_history(self, url): + self.location(url) + + if not self.is_on_page(AccountHistory): + # TODO: support other kind of accounts + return iter([]) + + return self.page.iter_transactions() diff --git a/modules/fortuneo/pages/__init__.py b/modules/fortuneo/pages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/fortuneo/pages/accounts_list.py b/modules/fortuneo/pages/accounts_list.py new file mode 100644 index 00000000..a072ea7b --- /dev/null +++ b/modules/fortuneo/pages/accounts_list.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Gilles-Alexandre Quenot +# +# 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 urlparse import parse_qs, urlparse +from lxml.etree import XML +from cStringIO import StringIO +from decimal import Decimal +import re + +from weboob.capabilities.bank import Account +from weboob.tools.capabilities.bank.transactions import FrenchTransaction +from weboob.tools.browser import BasePage, BrokenPageError + + +__all__ = ['AccountsList', 'AccountHistory'] + + +class AccountsList(BasePage): + def on_loaded(self): + pass + + def get_list(self): + l = [] + #for el in self.document.xpath('//table[@id="tableauComptesTitEtCotit"]/tbody/'): + #l.append('test') + #l.append('Livret +') + #l.append('20') + #l.append('https://www.fortuneo.fr/fr/prive/mes-comptes/livret/caracteristiques-mon-compte/?COMPTE_ACTIF=FT00654224521421145') + account.label = "test" + account.id = "Livret +" + account.balance = "20" + account._link_id = "https://www.fortuneo.fr/fr/prive/mes-comptes/livret/caracteristiques-mon-compte/?COMPTE_ACTIF=FT00654224521421145" + l.append(account) + #for tr in self.document.getiterator('tr'): + # if 'LGNTableRow' in tr.attrib.get('class', '').split(): + # account = Account() + # for td in tr.getiterator('td'): + # if td.attrib.get('headers', '') == 'TypeCompte': + # a = td.find('a') + # account.label = unicode(a.find("span").text) + # account._link_id = a.get('href', '') + + # elif td.attrib.get('headers', '') == 'NumeroCompte': + # id = td.text + # id = id.replace(u'\xa0','') + # account.id = id + + # elif td.attrib.get('headers', '') == 'Libelle': + # pass + + # elif td.attrib.get('headers', '') == 'Solde': + # balance = td.find('div').text + # if balance != None: + # balance = balance.replace(u'\xa0','').replace(',','.') + # account.balance = Decimal(balance) + # else: + # account.balance = Decimal(0) + + # l.append(account) + + return l + +class Transaction(FrenchTransaction): + print "DEBUG a implementer" + pass + #PATTERNS = [(re.compile(r'^CARTE \w+ RETRAIT DAB.* (?P
\d{2})/(?P\d{2}) (?P\d+)H(?P\d+) (?P.*)'), + # FrenchTransaction.TYPE_WITHDRAWAL), + # (re.compile(r'^(?PCARTE) \w+ (?P
\d{2})/(?P\d{2}) (?P.*)'), + # FrenchTransaction.TYPE_CARD), + # (re.compile(r'^(?P(COTISATION|PRELEVEMENT|TELEREGLEMENT|TIP)) (?P.*)'), + # FrenchTransaction.TYPE_ORDER), + # (re.compile(r'^(?PVIR(EMEN)?T? \w+) (?P.*)'), + # FrenchTransaction.TYPE_TRANSFER), + # (re.compile(r'^(CHEQUE) (?P.*)'), FrenchTransaction.TYPE_CHECK), + # (re.compile(r'^(FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK), + # (re.compile(r'^(?PECHEANCEPRET)(?P.*)'), + # FrenchTransaction.TYPE_LOAN_PAYMENT), + # (re.compile(r'^(?PREMISE CHEQUES)(?P.*)'), + # FrenchTransaction.TYPE_DEPOSIT), + # ] + +class AccountHistory(BasePage): + def get_part_url(self): + print "DEBUG a implementer" + pass + #for script in self.document.getiterator('script'): + # if script.text is None: + # continue + + # m = re.search('var listeEcrCavXmlUrl="(.*)";', script.text) + # if m: + # return m.group(1) + + #raise BrokenPageError('Unable to find link to history part') + + def iter_transactions(self): + print "DEBUG a implementer" + pass + #url = self.get_part_url() + #while 1: + # d = XML(self.browser.readurl(url)) + # el = d.xpath('//dataBody')[0] + # s = StringIO(el.text) + # doc = self.browser.get_document(s) + + # for tr in self._iter_transactions(doc): + # yield tr + + # el = d.xpath('//dataHeader')[0] + # if int(el.find('suite').text) != 1: + # return + + # url = urlparse(url) + # p = parse_qs(url.query) + # url = self.browser.buildurl(url.path, n10_nrowcolor=0, + # operationNumberPG=el.find('operationNumber').text, + # operationTypePG=el.find('operationType').text, + # pageNumberPG=el.find('pageNumber').text, + # sign=p['sign'][0], + # src=p['src'][0]) + + + def _iter_transactions(self, doc): + print "DEBUG a implementer" + pass + #for i, tr in enumerate(self.parser.select(doc.getroot(), 'tr')): + # t = Transaction(i) + # t.parse(date=tr.xpath('./td[@headers="Date"]')[0].text, + # raw=tr.attrib['title'].strip()) + # t.set_amount(*reversed([el.text for el in tr.xpath('./td[@class="right"]')])) + # t._coming = tr.xpath('./td[@headers="AVenir"]')[0].text + # yield t diff --git a/modules/fortuneo/pages/login.py b/modules/fortuneo/pages/login.py new file mode 100644 index 00000000..44542fcb --- /dev/null +++ b/modules/fortuneo/pages/login.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Gilles-Alexandre Quenot +# +# 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 logging import error + +from weboob.tools.browser import BasePage, BrowserUnavailable +#from lxml import etree + + +__all__ = ['LoginPage'] + +def dump(obj): + for attr in dir(obj): + print "obj.%s = %s" % (attr, getattr(obj, attr)) + +class LoginPage(BasePage): + def login(self, login, passwd): + #print "DEBUG BasePage=", BasePage.url + #dump(BasePage) + self.browser.select_form(nr=3) + #self.browser['locale'] = 'fr' + self.browser['login'] = login + self.browser['passwd'] = passwd + #self.browser['idDyn'] = 'false' + self.browser.submit() + #print "DEBUG ", self.page + +#class LoginPage(BasePage): +# def on_loaded(self): +# pass +# #for td in self.document.getroot().cssselect('td.LibelleErreur'): +# # if td.text is None: +# # continue +# # msg = td.text.strip() +# # if 'indisponible' in msg: +# # raise BrowserUnavailable(msg) +# +# def login(self, login, password): +# DOMAIN_LOGIN = self.browser.DOMAIN_LOGIN +# DOMAIN = self.browser.DOMAIN +# +# url_login = 'https://' + DOMAIN_LOGIN + '/index.html' +# +# base_url = 'https://' + DOMAIN +# url = base_url + '/cvcsgenclavier?mode=jsom&estSession=0' +# headers = { +# 'Referer': url_login +# } +# request = self.browser.request_class(url, None, headers) +# infos_data = self.browser.readurl(request) +# infos_xml = etree.XML(infos_data) +# infos = {} +# for el in ("cryptogramme", "nblignes", "nbcolonnes"): +# infos[el] = infos_xml.find(el).text +# +# infos["grille"] = "" +# for g in infos_xml.findall("grille"): +# infos["grille"] += g.text + "," +# infos["keyCodes"] = infos["grille"].split(",") +# +# url = base_url + '/cvcsgenimage?modeClavier=0&cryptogramme=' + infos["cryptogramme"] +# img = Captcha(self.browser.openurl(url), infos) +# +# try: +# img.build_tiles() +# except TileError, err: +# error("Error: %s" % err) +# if err.tile: +# err.tile.display() +# +# self.browser.openurl(url_login) +# self.browser.select_form('authentification') +# self.browser.set_all_readonly(False) +# +# self.browser['codcli'] = login +# self.browser['codsec'] = img.get_codes(password) +# self.browser['cryptocvcs'] = infos["cryptogramme"] +# self.browser.submit() + + +class BadLoginPage(BasePage): + pass diff --git a/modules/fortuneo/test.py b/modules/fortuneo/test.py new file mode 100644 index 00000000..d4d243c3 --- /dev/null +++ b/modules/fortuneo/test.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Gilles-Alexandre Quenot +# +# 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 FortuneoTest(BackendTest): + BACKEND = 'fortuneo' + + def test_fortuneo(self): + l = list(self.backend.iter_accounts()) + self.assertTrue(len(l) > 0) + a = l[0] + list(self.backend.iter_coming(a)) + list(self.backend.iter_history(a))