pax_global_header 0000666 0000000 0000000 00000000064 13435444765 0014531 g ustar 00root root 0000000 0000000 52 comment=139760e841ad4875b59fe11e052af1bb56dc06ea
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/ 0000775 0000000 0000000 00000000000 13435444765 0023307 5 ustar 00root root 0000000 0000000 woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/ 0000775 0000000 0000000 00000000000 13435444765 0024757 5 ustar 00root root 0000000 0000000 woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/linebourse/ 0000775 0000000 0000000 00000000000 13435444765 0027126 5 ustar 00root root 0000000 0000000 woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/linebourse/__init__.py 0000664 0000000 0000000 00000001577 13435444765 0031251 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2017 Vincent Ardisson
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from __future__ import unicode_literals
from .module import LinebourseModule
__all__ = ['LinebourseModule']
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/linebourse/api/ 0000775 0000000 0000000 00000000000 13435444765 0027677 5 ustar 00root root 0000000 0000000 woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/linebourse/api/__init__.py 0000664 0000000 0000000 00000000000 13435444765 0031776 0 ustar 00root root 0000000 0000000 woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/linebourse/api/pages.py 0000664 0000000 0000000 00000011573 13435444765 0031357 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2018 Fong Ngo
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from __future__ import unicode_literals
from weboob.browser.elements import method, DictElement, ItemElement
from weboob.browser.filters.json import Dict
from weboob.browser.filters.standard import (
Date, CleanDecimal, Eval, Field, Env, Regexp, Format,
)
from weboob.browser.pages import JsonPage, HTMLPage, LoggedPage
from weboob.capabilities.bank import Investment, Transaction
from weboob.capabilities.base import NotAvailable
from weboob.tools.capabilities.bank.investments import is_isin_valid
class AccountPage(LoggedPage, JsonPage):
def get_ncontrat(self):
return self.doc['identifiantContratCrypte']
class PortfolioPage(LoggedPage, JsonPage):
def get_valuation_diff(self):
return CleanDecimal(Dict('totalPlv'))(self.doc) # Plv = plus-value
def get_date(self):
return Date(Regexp(Dict('dateValo'), r'(\d{2})(\d{2})(\d{2})', '\\3\\2\\1'), dayfirst=True)(self.doc)
@method
class iter_investments(DictElement):
item_xpath = 'listeSegmentation/*' # all categories are fetched: obligations, actions, OPC
class item(ItemElement):
klass = Investment
obj_label = Dict('libval')
obj_code = Dict('codval')
obj_code_type = Eval(
lambda x: Investment.CODE_TYPE_ISIN if is_isin_valid(x) else NotAvailable,
Field('code')
)
obj_quantity = CleanDecimal(Dict('qttit'))
obj_unitvalue = CleanDecimal(Dict('crs'))
obj_valuation = CleanDecimal(Dict('mnt'))
obj_vdate = Env('date')
obj_portfolio_share = Eval(lambda x: x / 100, CleanDecimal(Dict('pourcentageActif')))
def parse(self, el):
symbols = {
'+': 1,
'-': -1,
'\u0000': None, # "NULL" character
}
self.env['sign'] = symbols.get(Dict('signePlv')(self), None)
def obj_diff(self):
if Dict('plv', default=None)(self) and Env('sign')(self):
return CleanDecimal(Dict('plv'), sign=lambda x: Env('sign')(self))(self)
return NotAvailable
def obj_unitprice(self):
if Dict('pam', default=None)(self):
return CleanDecimal(Dict('pam'))(self)
return NotAvailable
def obj_diff_percent(self):
if not Env('sign')(self):
return NotAvailable
# obj_diff_percent key can have several names:
if Dict('plvPourcentage', default=None)(self):
return CleanDecimal(Dict('plvPourcentage'), sign=lambda x: Env('sign')(self))(self)
elif Dict('pourcentagePlv', default=None)(self):
return CleanDecimal(Dict('pourcentagePlv'), sign=lambda x: Env('sign')(self))(self)
class AccountCodesPage(LoggedPage, JsonPage):
def get_contract_number(self, account_id):
for acc in self.doc['data']:
if account_id in acc['affichage']:
return acc['identifiantContratCrypte']
assert False, 'the account code was not found in the linebourse API'
class NewWebsiteFirstConnectionPage(LoggedPage, JsonPage):
def build_doc(self, content):
content = JsonPage.build_doc(self, content)
if 'data' in content:
# The value contains HTML
# Must be encoded into str because HTMLPage.build_doc() uses BytesIO
# which expects bytes
html_page = HTMLPage(self.browser, self.response)
return html_page.build_doc(content['data'].encode(self.encoding))
return content
class HistoryAPIPage(LoggedPage, JsonPage):
@method
class iter_history(DictElement):
item_xpath = 'data/lstOperations'
class item(ItemElement):
klass = Transaction
obj_label = Format('%s %s (%s)', Dict('libNatureOperation'), Dict('libValeur'), Dict('codeValeur'))
obj_amount = CleanDecimal(Dict('mntNet'))
obj_date = Date(Dict('dtOperation'))
obj_rdate = Date(Dict('dtOperation'))
obj_type = Transaction.TYPE_BANK
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/linebourse/browser.py 0000664 0000000 0000000 00000012570 13435444765 0031170 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2017 Vincent Ardisson
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from __future__ import unicode_literals
import time
from weboob.browser import LoginBrowser, URL
from weboob.exceptions import BrowserUnavailable
from .pages import (
MessagePage, InvestmentPage, HistoryPage, BrokenPage,
MainPage, FirstConnectionPage,
)
from .api.pages import (
PortfolioPage, NewWebsiteFirstConnectionPage,
AccountCodesPage, HistoryAPIPage,
)
def get_timestamp():
return '{}'.format(int(time.time() * 1000)) # in milliseconds
class LinebourseBrowser(LoginBrowser):
BASEURL = 'https://www.linebourse.fr'
main = URL(r'/Main$', MainPage)
first = URL(r'/GuidesPremiereConnexion$', FirstConnectionPage)
invest = URL(r'/Portefeuille$', r'/Portefeuille\?compte=(?P[^&]+)', InvestmentPage)
message = URL(r'/DetailMessage.*', MessagePage)
history = URL(r'/HistoriqueOperations',
r'/HistoriqueOperations\?compte=(?P[^&]+)&devise=EUR&modeTri=7&sensTri=-1&periode=(?P\d+)', HistoryPage)
useless = URL(r'/ReroutageSJR', MessagePage)
broken = URL(r'.*/timeout.html$', BrokenPage)
def __init__(self, baseurl, *args, **kwargs):
super(LinebourseBrowser, self).__init__('', '', *args, **kwargs)
self.BASEURL = baseurl
def do_login(self):
raise BrowserUnavailable()
def iter_investment(self, account_id):
self.main.go()
self.invest.go()
if self.message.is_here():
self.page.submit()
self.invest.go()
if self.broken.is_here():
return iter([])
assert self.invest.is_here()
if not self.page.is_on_right_portfolio(account_id):
self.invest.go(id=self.page.get_compte(account_id))
return self.page.iter_investments()
# Method used only by bp module
def get_liquidity(self, account_id):
self.main.go()
self.invest.go()
if self.message.is_here():
self.page.submit()
self.invest.go()
if self.broken.is_here():
return iter([])
assert self.invest.is_here()
if not self.page.is_on_right_portfolio(account_id):
self.invest.go(id=self.page.get_compte(account_id))
return self.page.get_liquidity()
def iter_history(self, account_id):
self.main.go()
self.history.go()
if self.message.is_here():
self.page.submit()
self.history.go()
if self.broken.is_here():
return
assert self.history.is_here()
if not self.page.is_on_right_portfolio(account_id):
self.history.go(id=self.page.get_compte(account_id), period=0)
periods = self.page.get_periods()
for period in periods:
self.history.go(id=self.page.get_compte(account_id), period=period)
for tr in self.page.iter_history():
yield tr
class LinebourseAPIBrowser(LoginBrowser):
BASEURL = 'https://www.offrebourse.com'
new_website_first = URL(r'/rest/premiereConnexion', NewWebsiteFirstConnectionPage)
account_codes = URL(r'/rest/compte/liste/vide/0', AccountCodesPage)
# The API works with an encrypted account_code that starts with 'CRY'
portfolio = URL(r'/rest/portefeuille/(?PCRY[\w\d]+)/vide/true/false', PortfolioPage)
history = URL(r'/rest/historiqueOperations/(?PCRY[\w\d]+)/0/7/1', HistoryAPIPage) # TODO: not sure if last 3 path levels can be hardcoded
def __init__(self, baseurl, *args, **kwargs):
self.BASEURL = baseurl
super(LinebourseAPIBrowser, self).__init__(username='', password='', *args, **kwargs)
def get_account_code(self, account_id):
# 'account_codes' is a JSON containing the id_contracts
# of all the accounts present on the Linebourse space.
params = {'_': get_timestamp()}
self.account_codes.go(params=params)
assert self.account_codes.is_here()
return self.page.get_contract_number(account_id)
def go_portfolio(self, account_id):
account_code = self.get_account_code(account_id)
return self.portfolio.go(account_code=account_code)
def iter_investments(self, account_id):
self.go_portfolio(account_id)
assert self.portfolio.is_here()
date = self.page.get_date()
return self.page.iter_investments(date=date)
def iter_history(self, account_id):
account_code = self.get_account_code(account_id)
self.history.go(
account_code=account_code,
params={'_': get_timestamp()}, # timestamp is necessary
)
assert self.history.is_here()
for tr in self.page.iter_history():
yield tr
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/linebourse/module.py 0000664 0000000 0000000 00000002337 13435444765 0030772 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2017 Vincent Ardisson
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from __future__ import unicode_literals
from weboob.tools.backend import Module
from weboob.capabilities.bank import CapBank
from .browser import LinebourseBrowser
__all__ = ['LinebourseModule']
class LinebourseModule(Module, CapBank):
NAME = 'linebourse'
DESCRIPTION = u'linebourse website'
MAINTAINER = u'Vincent Ardisson'
EMAIL = 'vardisson@budget-insight.com'
LICENSE = 'AGPLv3+'
VERSION = '1.5'
BROWSER = LinebourseBrowser
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-linebourse/modules/linebourse/pages.py 0000664 0000000 0000000 00000014052 13435444765 0030601 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2017 Vincent Ardisson
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from __future__ import unicode_literals
import re
from weboob.browser.pages import HTMLPage, LoggedPage
from weboob.browser.elements import method, TableElement, ItemElement
from weboob.browser.filters.standard import (
CleanText, Date, CleanDecimal, Regexp, Eval, Field
)
from weboob.browser.filters.html import TableCell
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.bank import Investment
from weboob.tools.capabilities.bank.transactions import FrenchTransaction as Transaction
from weboob.tools.capabilities.bank.investments import create_french_liquidity
from weboob.tools.compat import quote_plus
from weboob.exceptions import ActionNeeded
def MyDecimal(*args, **kwargs):
kwargs['replace_dots'] = True
return CleanDecimal(*args, **kwargs)
class MainPage(LoggedPage, HTMLPage):
pass
class FirstConnectionPage(LoggedPage, HTMLPage):
def on_load(self):
raise ActionNeeded(CleanText('//p[contains(text(), "prendre connaissance")]')(self.doc))
class AccountPage(LoggedPage, HTMLPage):
def is_on_right_portfolio(self, account_id):
return len(self.doc.xpath('//form[@class="choixCompte"]//option[@selected and contains(text(), $id)]', id=account_id))
def get_compte(self, account_id):
values = self.doc.xpath('//option[contains(text(), $id)]/@value', id=account_id)
assert len(values) == 1, 'could not find account %r' % account_id
return quote_plus(values[0])
class HistoryPage(AccountPage):
def get_periods(self):
return list(self.doc.xpath('//select[@id="ListeDate"]/option/@value'))
@method
class iter_history(TableElement):
col_date = 'Date'
col_name = 'Valeur'
col_quantity = u'Quantité'
col_amount = u'Montant net EUR'
col_label = u'Opération'
head_xpath = u'//table[@summary="Historique operations"]//tr[th]/th'
item_xpath = u'//table[@summary="Historique operations"]//tr[not(th)]'
def parse(self, el):
self.labels = {}
class item(ItemElement):
def condition(self):
text = CleanText('td')(self)
return not text.startswith('Aucune information disponible')
klass = Transaction
obj_date = Date(CleanText(TableCell('date')), dayfirst=True)
obj_amount = MyDecimal(TableCell('amount'))
obj_raw = CleanText(TableCell('label'))
def obj_investments(self):
inv = Investment()
inv.quantity = CleanDecimal(TableCell('quantity'), replace_dots=True)(self)
inv.code_type = Investment.CODE_TYPE_ISIN
txt = CleanText(TableCell('name'))(self)
match = re.match('(?:(.*) )?- ([^-]+)$', txt)
inv.label = match.group(1) or NotAvailable
inv.code = match.group(2)
if inv.code in self.parent.labels:
inv.label = inv.label or self.parent.labels[inv.code]
elif inv.label:
self.parent.labels[inv.code] = inv.label
else:
inv.label = inv.code
return [inv]
class InvestmentPage(AccountPage):
@method
class iter_investments(TableElement):
col_label = 'Valeur'
col_quantity = u'Quantité'
col_valuation = u'Valorisation EUR'
col_unitvalue = 'Cours/VL'
col_unitprice = 'Prix moyen EUR'
col_portfolio_share = '% Actif'
col_diff = u'+/- value latente EUR'
head_xpath = u'//table[starts-with(@summary,"Contenu du portefeuille")]/thead//th'
item_xpath = u'//table[starts-with(@summary,"Contenu du portefeuille")]/tbody/tr[2]'
class item(ItemElement):
klass = Investment
def condition(self):
return Field('quantity')(self) != NotAvailable and Field('quantity')(self) > 0
obj_quantity = MyDecimal(TableCell('quantity'), default=NotAvailable)
obj_unitvalue = MyDecimal(TableCell('unitvalue'), default=NotAvailable)
obj_unitprice = MyDecimal(TableCell('unitprice'), default=NotAvailable)
obj_valuation = MyDecimal(TableCell('valuation'), default=NotAvailable)
obj_portfolio_share = Eval(lambda x: x / 100 if x else NotAvailable, MyDecimal(TableCell('portfolio_share'), default=NotAvailable))
obj_diff = MyDecimal(TableCell('diff', default=NotAvailable), default=NotAvailable)
obj_code_type = Investment.CODE_TYPE_ISIN
obj_label = CleanText(Regexp(CleanText('./preceding-sibling::tr/td[1]'), '(.*)- .*'))
obj_code = Regexp(CleanText('./preceding-sibling::tr/td[1]'), '- (.*)')
# Only used by bp modules since others quality websites provide another account with the liquidities
def get_liquidity(self):
liquidity = CleanDecimal('//table//tr[@class="titreAvant"]/td[contains(text(), "Liquidit")]/following-sibling::td', replace_dots=True)(self.doc)
if liquidity:
return create_french_liquidity(liquidity)
class MessagePage(LoggedPage, HTMLPage):
def submit(self):
# taken from linebourse implementation in caissedepargne module
form = self.get_form(name='leForm')
form['signatur1'] = 'on'
form.submit()
class BrokenPage(LoggedPage, HTMLPage):
pass