pax_global_header 0000666 0000000 0000000 00000000064 13067703106 0014515 g ustar 00root root 0000000 0000000 52 comment=e5d2d33d4673f46720dc816ea0e1f542ae9e11fb
woob-e5d2d33d4673f46720dc816ea0e1f542ae9e11fb-modules-spirica/ 0000775 0000000 0000000 00000000000 13067703106 0022636 5 ustar 00root root 0000000 0000000 woob-e5d2d33d4673f46720dc816ea0e1f542ae9e11fb-modules-spirica/modules/ 0000775 0000000 0000000 00000000000 13067703106 0024306 5 ustar 00root root 0000000 0000000 woob-e5d2d33d4673f46720dc816ea0e1f542ae9e11fb-modules-spirica/modules/spirica/ 0000775 0000000 0000000 00000000000 13067703106 0025740 5 ustar 00root root 0000000 0000000 woob-e5d2d33d4673f46720dc816ea0e1f542ae9e11fb-modules-spirica/modules/spirica/__init__.py 0000664 0000000 0000000 00000001442 13067703106 0030052 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Edouard Lambert
#
# 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 .module import SpiricaModule
__all__ = ['SpiricaModule']
woob-e5d2d33d4673f46720dc816ea0e1f542ae9e11fb-modules-spirica/modules/spirica/browser.py 0000664 0000000 0000000 00000006167 13067703106 0030007 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Edouard Lambert
#
# 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.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword
from .pages import LoginPage, AccountsPage, DetailsPage, MaintenancePage
class SpiricaBrowser(LoginBrowser):
TIMEOUT = 60
login = URL('/securite/login.xhtml', LoginPage)
accounts = URL('/sylvea/client/synthese.xhtml', AccountsPage)
details = URL('/sylvea/contrat/consultationContratEpargne.xhtml', DetailsPage)
maintenance = URL('/maintenance.html', MaintenancePage)
def __init__(self, website, *args, **kwargs):
super(SpiricaBrowser, self).__init__(*args, **kwargs)
self.BASEURL = website
self.cache = {}
self.cache['invs'] = {}
self.cache['trs'] = {}
def do_login(self):
self.login.go().login(self.username, self.password)
if self.login.is_here():
error = self.page.get_error()
raise BrowserIncorrectPassword(error)
def get_subscription_list(self):
return iter([])
@need_login
def iter_accounts(self):
if 'accs' not in self.cache.keys():
self.cache['accs'] = [a for a in self.accounts.stay_or_go().iter_accounts()]
return self.cache['accs']
@need_login
def iter_investment(self, account):
if account.id not in self.cache['invs']:
# Get form to show PRM
form = self.location(account._link).page.get_investment_form()
invs = [i for i in self.location(form.url, data=dict(form)).page.iter_investment()]
self.cache['invs'][account.id] = invs
return self.cache['invs'][account.id]
@need_login
def iter_history(self, account):
if account.id not in self.cache['trs']:
# Get form to go to History's tab
form = self.location(account._link).page.get_historytab_form()
# Get form to show all transactions
form = self.location(form.url, data=dict(form)).page.get_historyallpages_form()
if form:
self.location(form.url, data=dict(form))
# Get forms to expand details of all transactions
for form in self.page.get_historyexpandall_form():
# Can't async because of ReadTimeout
self.location(form.url, data=dict(form))
trs = [t for t in self.page.iter_history()]
self.cache['trs'][account.id] = trs
return self.cache['trs'][account.id]
woob-e5d2d33d4673f46720dc816ea0e1f542ae9e11fb-modules-spirica/modules/spirica/module.py 0000664 0000000 0000000 00000003671 13067703106 0027606 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Edouard Lambert
#
# 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.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword
from weboob.capabilities.bank import CapBank, AccountNotFound
from weboob.capabilities.base import find_object
from .browser import SpiricaBrowser
__all__ = ['SpiricaModule']
class SpiricaModule(Module, CapBank):
NAME = 'spirica'
DESCRIPTION = u'Spirica'
MAINTAINER = u'Edouard Lambert'
EMAIL = 'elambert@budget-insight.com'
LICENSE = 'AGPLv3+'
VERSION = '1.3'
CONFIG = BackendConfig(
ValueBackendPassword('login', label='Identifiant', masked=False),
ValueBackendPassword('password', label='Mot de passe'))
BROWSER = SpiricaBrowser
def create_default_browser(self):
return self.create_browser("https://www.sylvea.fr", self.config['login'].get(), self.config['password'].get())
def get_account(self, _id):
return find_object(self.browser.iter_accounts(), id=_id, error=AccountNotFound)
def iter_accounts(self):
return self.browser.iter_accounts()
def iter_history(self, account):
return self.browser.iter_history(account)
def iter_investment(self, account):
return self.browser.iter_investment(account)
woob-e5d2d33d4673f46720dc816ea0e1f542ae9e11fb-modules-spirica/modules/spirica/pages.py 0000664 0000000 0000000 00000023705 13067703106 0027420 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Edouard Lambert
#
# 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 re
from weboob.browser.pages import HTMLPage, LoggedPage
from weboob.browser.elements import ItemElement, TableElement, method
from weboob.browser.filters.standard import CleanText, Date, Regexp, CleanDecimal, \
TableCell, Field, Async, AsyncLoad, Eval
from weboob.browser.filters.html import Attr, Link
from weboob.capabilities.bank import Account, Investment, Transaction
from weboob.capabilities.base import NotAvailable, empty
from weboob.exceptions import BrowserUnavailable
def MyDecimal(*args, **kwargs):
kwargs.update(replace_dots=True, default=NotAvailable)
return CleanDecimal(*args, **kwargs)
class MaintenancePage(HTMLPage):
def on_load(self):
raise BrowserUnavailable(CleanText().filter(self.doc.xpath('//p')))
class LoginPage(HTMLPage):
def login(self, login, password):
form = self.get_form('//form[@id="loginForm"]')
form['loginForm:name'] = login
form['loginForm:password'] = password
form['loginForm:login'] = "loginForm:login"
form.submit()
def get_error(self):
return CleanText('//li[@class="erreurBox"]')(self.doc)
class AccountsPage(LoggedPage, HTMLPage):
TYPES = {
'Assurance Vie': Account.TYPE_LIFE_INSURANCE,
'Capitalisation': Account.TYPE_MARKET,
'Unknown': Account.TYPE_UNKNOWN,
}
@method
class iter_accounts(TableElement):
item_xpath = '//table[@role]/tbody/tr'
head_xpath = '//table[@role]/thead/tr/th'
col_label = u'Produit'
col_id = u'Numéro de contrat'
col_balance = u'Montant (€)'
class item(ItemElement):
klass = Account
load_details = Field('_link') & AsyncLoad
obj_id = CleanText(TableCell('id'), replace=[(' ', '')])
obj_label = CleanText(TableCell('label'))
obj_balance = MyDecimal(TableCell('balance'))
obj_valuation_diff = Async('details') & MyDecimal('//tr[1]/td[contains(text(), \
"value du contrat")]/following-sibling::td')
obj__link = Link('.//a')
def obj_type(self):
return self.page.TYPES[Async('details', CleanText('//td[contains(text(), \
"Option fiscale")]/following-sibling::td', default="Unknown"))(self)]
class TableInvestment(TableElement):
col_label = u'Support'
col_vdate = u'Date de valeur'
col_unitvalue = u'Valeur de part'
col_quantity = u'Nombre de parts'
col_portfolio_share = u'%'
class ItemInvestment(ItemElement):
klass = Investment
obj_label = CleanText(TableCell('label'))
obj_quantity = MyDecimal(TableCell('quantity', default=None))
obj_unitvalue = MyDecimal(TableCell('unitvalue', default=None))
obj_vdate = Date(CleanText(TableCell('vdate', default="")), dayfirst=True, default=NotAvailable)
def obj_valuation(self):
valuation = MyDecimal(TableCell('valuation', default=None))(self)
h2 = CleanText('./ancestor::div[contains(@id, "Histo")][1]/preceding-sibling::h2[1]')(self)
return -valuation if valuation and any(word in h2.lower() for word in self.page.DEBIT_WORDS) else valuation
def obj_portfolio_share(self):
ps = MyDecimal(TableCell('portfolio_share', default=None))(self)
return Eval(lambda x: x / 100, ps)(self) if not empty(ps) else NotAvailable
class TableTransactionsInvestment(TableInvestment):
item_xpath = './tbody/tr'
head_xpath = './thead/tr/th'
col_code = u'ISIN'
col_valuation = [u'Montant brut', u'Montant net']
class item(ItemInvestment):
obj_code = Regexp(CleanText(TableCell('code')), pattern='([A-Z]{2}\d{10})', default=NotAvailable)
class ProfileTableInvestment(TableInvestment):
# used only when portfolio is divided in multiple "profiles"
head_xpath = '//thead[ends-with(@id, ":contratProfilTable_head")]/tr/th'
class DetailsPage(LoggedPage, HTMLPage):
DEBIT_WORDS = [u'arrêté', 'rachat', 'frais', u'désinvestir']
def get_investment_form(self):
form = self.get_form('//form[contains(@id, "j_idt")]')
form['ongletSituation:ongletContratTab_newTab'] = \
Link().filter(self.doc.xpath('//a[contains(text(), "Prix de revient moyen")]'))[1:]
form['javax.faces.source'] = "ongletSituation:ongletContratTab"
form['javax.faces.behavior.event'] = "tabChange"
return form
@method
class iter_investment(TableInvestment):
item_xpath = '//div[contains(@id,"INVESTISSEMENT")]//div[ends-with(@id, ":tableDetailSituationCompte")]//table/tbody/tr[@data-ri]'
head_xpath = '//div[contains(@id,"INVESTISSEMENT")]//div[ends-with(@id, ":tableDetailSituationCompte")]//table/thead/tr/th'
col_valuation = re.compile('Contre')
class item(ItemInvestment):
obj_code = Regexp(CleanText('.//td[contains(text(), "Isin")]'), ':[\s]+([\w]+)', default=NotAvailable)
def obj_unitprice(self):
return MyDecimal('//div[contains(@id, "PRIX_REVIENT")]//a[contains(text(), \
"%s")]/ancestor::tr/td[5]' % Field('label')(self))(self)
def obj_diff(self):
return MyDecimal('//div[contains(@id, "PRIX_REVIENT")]//a[contains(text(), \
"%s")]/ancestor::tr/td[6]' % Field('label')(self))(self)
def obj_portfolio_share(self):
inv_share = ItemInvestment.obj_portfolio_share(self)
if self.xpath('ancestor::tbody[ends-with(@id, "contratProfilTable_data")]'):
# investments are nested in profiles, row share is relative to profile share
profile_table_el = self.xpath('ancestor::tr/ancestor::table[position() = 1]')[0]
profile_table = ProfileTableInvestment(self.page, self, profile_table_el)
share_idx = profile_table.get_colnum('portfolio_share')
assert share_idx
path = 'ancestor::tr/preceding-sibling::tr[@data-ri][position() = 1][1]/td[%d]' % (share_idx + 1)
profile_share = MyDecimal(path)(self)
assert profile_share
#raise Exception('dtc')
profile_share = Eval(lambda x: x / 100, profile_share)(self)
return inv_share * profile_share
else:
return inv_share
def get_historytab_form(self):
form = self.get_form('//form[contains(@id, "j_idt")]')
idt = Attr(None, 'name').filter(self.doc.xpath('//input[contains(@name, "j_idt") \
and contains(@name, "activeIndex")]')).rsplit('_', 1)[0]
form['%s_contentLoad' % idt] = "true"
form['%s_newTab' % idt] = Link().filter(self.doc.xpath('//a[contains(@href, "HISTORIQUE")]'))[1:]
form['%s_activeIndex' % idt] = "1"
form['javax.faces.source'] = idt
form['javax.faces.behavior.event'] = "tabChange"
return form
def get_historyallpages_form(self):
onclick = self.doc.xpath('//a[contains(text(), "Tout")]/@onclick')
if onclick:
idt = re.search('{[^\w]+([\w\d:]+)', onclick[0]).group(1)
form = self.get_form('//form[contains(@id, "j_idt")]')
form[idt] = idt
return form
return False
def get_historyexpandall_form(self):
form = self.get_form('//form[contains(@id, "j_idt")]')
form['javax.faces.source'] = "ongletHistoOperations:newoperations"
form['javax.faces.behavior.event'] = "rowToggle"
form['ongletHistoOperations:newoperations_rowExpansion'] = "true"
for data in self.doc.xpath('//tr[@data-ri]/@data-ri'):
form['ongletHistoOperations:newoperations_expandedRowIndex'] = data
yield form
@method
class iter_history(TableElement):
item_xpath = '//table/tbody[@id and not(contains(@id, "j_idt"))]/tr[@data-ri]'
head_xpath = '//table/thead[@id and not(contains(@id, "j_idt"))]/tr/th'
col_label = u'Type'
col_status = u'Etat'
col_brut = [u'Montant brut', u'Brut']
col_net = [u'Montant net', u'Net']
col_date = u'Date de réception'
col_vdate = u'Date de valeur'
class item(ItemElement):
klass = Transaction
obj_label = CleanText(TableCell('label'))
obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True)
obj_type = Transaction.TYPE_BANK
def obj_amount(self):
amount = MyDecimal(TableCell('net') if not CleanText(TableCell('brut'))(self) else TableCell('brut'))(self)
return -amount if amount and any(word in Field('label')(self).lower() for word in self.page.DEBIT_WORDS) else amount
def obj_date(self):
return Date(CleanText(TableCell('date')), dayfirst=True, default=Field('vdate')(self))(self)
def condition(self):
return u"Validé" in CleanText(TableCell('status'))(self)
def obj_investments(self):
investments = []
for table in self.el.xpath('./following-sibling::tr[1]//span[contains(text(), "ISIN")]/ancestor::table[1]'):
investments.extend(TableTransactionsInvestment(self.page, el=table)())
return investments
woob-e5d2d33d4673f46720dc816ea0e1f542ae9e11fb-modules-spirica/modules/spirica/test.py 0000664 0000000 0000000 00000001606 13067703106 0027274 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Edouard Lambert
#
# 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 SpiricaTest(BackendTest):
MODULE = 'spirica'
def test_spirica(self):
raise NotImplementedError()