Commit 5bcddf37 authored by ntome's avatar ntome

backport master module fixes

parent 6cc35952
Pipeline #2396 canceled with stages
...@@ -17,6 +17,7 @@ except AttributeError: ...@@ -17,6 +17,7 @@ except AttributeError:
# can't create a subclass because of CapBank.iter_resources reimplementations: # can't create a subclass because of CapBank.iter_resources reimplementations:
# modules will import our subclass, but boobank will call iter_resources with the OLD class # modules will import our subclass, but boobank will call iter_resources with the OLD class
Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account') Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Loan._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)') Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)')
......
...@@ -124,6 +124,18 @@ class RecaptchaQuestion(_RecaptchaQuestion): ...@@ -124,6 +124,18 @@ class RecaptchaQuestion(_RecaptchaQuestion):
super(RecaptchaQuestion, self).__init__(self.type, website_key=website_key, website_url=website_url) super(RecaptchaQuestion, self).__init__(self.type, website_key=website_key, website_url=website_url)
class RecaptchaV3Question(CaptchaQuestion):
type = 'g_recaptcha'
website_key = None
website_url = None
action = None
def __init__(self, website_key, website_url, action=None):
super(RecaptchaV3Question, self).__init__(self.type, website_key=website_key, website_url=website_url)
self.action = action
from weboob.exceptions import FuncaptchaQuestion as _FuncaptchaQuestion from weboob.exceptions import FuncaptchaQuestion as _FuncaptchaQuestion
class FuncaptchaQuestion(_FuncaptchaQuestion): class FuncaptchaQuestion(_FuncaptchaQuestion):
type = 'funcaptcha' type = 'funcaptcha'
......
...@@ -17,6 +17,7 @@ except AttributeError: ...@@ -17,6 +17,7 @@ except AttributeError:
# can't create a subclass because of CapBank.iter_resources reimplementations: # can't create a subclass because of CapBank.iter_resources reimplementations:
# modules will import our subclass, but boobank will call iter_resources with the OLD class # modules will import our subclass, but boobank will call iter_resources with the OLD class
Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account') Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Loan._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)') Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)')
......
...@@ -17,6 +17,7 @@ except AttributeError: ...@@ -17,6 +17,7 @@ except AttributeError:
# can't create a subclass because of CapBank.iter_resources reimplementations: # can't create a subclass because of CapBank.iter_resources reimplementations:
# modules will import our subclass, but boobank will call iter_resources with the OLD class # modules will import our subclass, but boobank will call iter_resources with the OLD class
Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account') Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Loan._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)') Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)')
......
...@@ -17,6 +17,7 @@ except AttributeError: ...@@ -17,6 +17,7 @@ except AttributeError:
# can't create a subclass because of CapBank.iter_resources reimplementations: # can't create a subclass because of CapBank.iter_resources reimplementations:
# modules will import our subclass, but boobank will call iter_resources with the OLD class # modules will import our subclass, but boobank will call iter_resources with the OLD class
Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account') Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Loan._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)') Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)')
......
...@@ -116,7 +116,10 @@ class AccountHistoryPage(LoggedPage, JsonPage): ...@@ -116,7 +116,10 @@ class AccountHistoryPage(LoggedPage, JsonPage):
if ('nomDispositif' in ins and 'montantNet' in ins and 'codeDispositif' in ins if ('nomDispositif' in ins and 'montantNet' in ins and 'codeDispositif' in ins
and '%s%s' % (ins['nomDispositif'], ins['codeDispositif']) and '%s%s' % (ins['nomDispositif'], ins['codeDispositif'])
== '%s%s' % (account.label, account.id)): == '%s%s' % (account.label, account.id)):
amount += ins['montantNet'] if ins['type'] == 'RACH_TIT':
amount -= ins['montantNet']
else:
amount += ins['montantNet']
return CleanDecimal().filter(amount) return CleanDecimal().filter(amount)
......
...@@ -23,8 +23,8 @@ from base64 import b64encode ...@@ -23,8 +23,8 @@ from base64 import b64encode
from weboob.browser.browsers import APIBrowser from weboob.browser.browsers import APIBrowser
from weboob.exceptions import BrowserIncorrectPassword, BrowserBanned from weboob.exceptions import BrowserIncorrectPassword, BrowserBanned
from weboob.capabilities.captcha import ( from .compat.weboob_capabilities_captcha import (
ImageCaptchaJob, RecaptchaJob, NocaptchaJob, FuncaptchaJob, CaptchaError, ImageCaptchaJob, RecaptchaJob, RecaptchaV3Job, NocaptchaJob, FuncaptchaJob, CaptchaError,
InsufficientFunds, UnsolvableCaptcha, InvalidCaptcha, InsufficientFunds, UnsolvableCaptcha, InvalidCaptcha,
) )
...@@ -74,6 +74,20 @@ class AnticaptchaBrowser(APIBrowser): ...@@ -74,6 +74,20 @@ class AnticaptchaBrowser(APIBrowser):
r = self.request('/createTask', data=data) r = self.request('/createTask', data=data)
return str(r['taskId']) return str(r['taskId'])
def post_gcaptchav3(self, url, key, action):
data = {
"clientKey": self.apikey,
"task":{
"type":"RecaptchaV3TaskProxyless",
"websiteURL": url,
"websiteKey": key,
"minScore": 0.3,
"pageAction": action
}
}
r = self.request('/createTask', data=data)
return str(r['taskId'])
def post_funcaptcha(self, url, key, sub_domain): def post_funcaptcha(self, url, key, sub_domain):
data = { data = {
"clientKey": self.apikey, "clientKey": self.apikey,
...@@ -128,7 +142,7 @@ class AnticaptchaBrowser(APIBrowser): ...@@ -128,7 +142,7 @@ class AnticaptchaBrowser(APIBrowser):
elif isinstance(job, RecaptchaJob): elif isinstance(job, RecaptchaJob):
job.solution = sol['recaptchaResponse'] job.solution = sol['recaptchaResponse']
job.solution_challenge = sol['recaptchaChallenge'] job.solution_challenge = sol['recaptchaChallenge']
elif isinstance(job, NocaptchaJob): elif isinstance(job, NocaptchaJob) or isinstance(job, RecaptchaV3Job):
job.solution = sol['gRecaptchaResponse'] job.solution = sol['gRecaptchaResponse']
elif isinstance(job, FuncaptchaJob): elif isinstance(job, FuncaptchaJob):
job.solution = sol['token'] job.solution = sol['token']
......
# -*- coding: utf-8 -*-
# Copyright(C) 2018 Vincent A
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from time import sleep
from weboob.capabilities.base import Capability, BaseObject, StringField, UserError, BytesField
from weboob.exceptions import (
RecaptchaQuestion, RecaptchaV3Question, NocaptchaQuestion, FuncaptchaQuestion, ImageCaptchaQuestion
)
__all__ = [
'CapCaptchaSolver',
'SolverJob', 'RecaptchaJob', 'NocaptchaJob', 'ImageCaptchaJob',
'CaptchaError', 'UnsolvableCaptcha', 'InvalidCaptcha', 'InsufficientFunds',
'exception_to_job',
]
from weboob.capabilities.captcha import SolverJob as _SolverJob
class SolverJob(_SolverJob):
solution = StringField('CAPTCHA solution')
from weboob.capabilities.captcha import RecaptchaJob as _RecaptchaJob
class RecaptchaJob(_RecaptchaJob):
site_url = StringField('Site URL for ReCaptcha service')
site_key = StringField('Site key for ReCaptcha service')
solution_challenge = StringField('Challenge ID of the solution (output value)')
class RecaptchaV3Job(SolverJob):
site_url = StringField('Site URL for ReCaptcha service')
site_key = StringField('Site key for ReCaptcha service')
action = StringField('Website owner defines what user is doing on the page through this parameter.')
from weboob.capabilities.captcha import NocaptchaJob as _NocaptchaJob
class NocaptchaJob(_NocaptchaJob):
site_url = StringField('Site URL for NoCaptcha service')
site_key = StringField('Site key for NoCaptcha service')
from weboob.capabilities.captcha import FuncaptchaJob as _FuncaptchaJob
class FuncaptchaJob(_FuncaptchaJob):
site_url = StringField('Site URL for FunCaptcha service')
site_key = StringField('Site key for FunCaptcha service')
sub_domain = StringField('Required for some complex cases, but Funcaptcha integrations run without it')
from weboob.capabilities.captcha import ImageCaptchaJob as _ImageCaptchaJob
class ImageCaptchaJob(_ImageCaptchaJob):
image = BytesField('data of the image to solve')
from weboob.capabilities.captcha import CaptchaError as _CaptchaError
class CaptchaError(_CaptchaError):
"""Generic solving error"""
from weboob.capabilities.captcha import InvalidCaptcha as _InvalidCaptcha
class InvalidCaptcha(_InvalidCaptcha):
"""CAPTCHA cannot be used (e.g. invalid image format)"""
from weboob.capabilities.captcha import UnsolvableCaptcha as _UnsolvableCaptcha
class UnsolvableCaptcha(_UnsolvableCaptcha):
"""CAPTCHA is too hard or impossible"""
from weboob.capabilities.captcha import InsufficientFunds as _InsufficientFunds
class InsufficientFunds(_InsufficientFunds):
"""Not enough funds to pay solution"""
def exception_to_job(exc):
if isinstance(exc, RecaptchaQuestion):
job = RecaptchaJob()
job.site_url = exc.website_url
job.site_key = exc.website_key
elif isinstance(exc, RecaptchaV3Question):
job = RecaptchaV3Job()
job.site_url = exc.website_url
job.site_key = exc.website_key
job.action = exc.action
elif isinstance(exc, NocaptchaQuestion):
job = NocaptchaJob()
job.site_url = exc.website_url
job.site_key = exc.website_key
elif isinstance(exc, FuncaptchaQuestion):
job = FuncaptchaJob()
job.site_url = exc.website_url
job.site_key = exc.website_key
job.sub_domain = exc.sub_domain
elif isinstance(exc, ImageCaptchaQuestion):
job = ImageCaptchaJob()
job.image = exc.image_data
else:
raise NotImplementedError()
return job
from weboob.capabilities.captcha import CapCaptchaSolver as _CapCaptchaSolver
class CapCaptchaSolver(_CapCaptchaSolver):
"""
Provide CAPTCHA solving
"""
RETRIES = 30
WAIT_TIME = 2
def create_job(self, job):
"""Start a CAPTCHA solving job
The `job.id` shall be filled. The CAPTCHA is not solved yet when the method returns.
:param job: job to start
:type job: :class:`SolverJob`
:raises: :class:`NotImplementedError` if CAPTCHA type is not supported
:raises: :class:`CaptchaError` in case of other error
"""
raise NotImplementedError()
def poll_job(self, job):
"""Check if a job was solved
If `job` is solved, return True and fill `job.solution`.
Return False if solution is still pending.
In case of solving problem, an exception may be raised.
It should not wait for the solution but return the current state.
:param job: job to check and to fill when solved
:type job: :class:`SolverJob`
:returns: True if the job was solved
:rtype: bool
:raises: :class:`CaptchaError`
"""
raise NotImplementedError()
def solve_catpcha_blocking(self, job):
"""Start a CAPTCHA solving job and wait for its solution
:param job: job to start and solve
:type job: :class:`SolverJob`
:raises: :class:`CaptchaError`
"""
self.create_job(job)
for i in range(self.RETRIES):
sleep(self.WAIT_TIME)
if self.poll_job(job):
return job
def report_wrong_solution(self, job):
"""Report a solved job as a wrong solution
Sometimes, jobs are solved, but the solution is rejected by the CAPTCHA
site because the solution is wrong.
This method reports the solution as wrong to the CAPTCHA solver.
:param job: job to flag
:type job: :class:`SolverJob`
"""
raise NotImplementedError()
def get_balance(self):
"""Get the prepaid balance left
:rtype: float
"""
raise NotImplementedError()
...@@ -21,7 +21,9 @@ from __future__ import unicode_literals ...@@ -21,7 +21,9 @@ from __future__ import unicode_literals
from weboob.tools.backend import Module, BackendConfig from weboob.tools.backend import Module, BackendConfig
from weboob.capabilities.captcha import CapCaptchaSolver, ImageCaptchaJob, RecaptchaJob, NocaptchaJob, FuncaptchaJob from .compat.weboob_capabilities_captcha import (
CapCaptchaSolver, ImageCaptchaJob, RecaptchaJob, RecaptchaV3Job, NocaptchaJob, FuncaptchaJob
)
from weboob.tools.value import ValueBackendPassword from weboob.tools.value import ValueBackendPassword
from .browser import AnticaptchaBrowser from .browser import AnticaptchaBrowser
...@@ -53,6 +55,8 @@ class AnticaptchaModule(Module, CapCaptchaSolver): ...@@ -53,6 +55,8 @@ class AnticaptchaModule(Module, CapCaptchaSolver):
job.id = self.browser.post_image(job.image) job.id = self.browser.post_image(job.image)
elif isinstance(job, RecaptchaJob): elif isinstance(job, RecaptchaJob):
job.id = self.browser.post_recaptcha(job.site_url, job.site_key) job.id = self.browser.post_recaptcha(job.site_url, job.site_key)
elif isinstance(job, RecaptchaV3Job):
job.id = self.browser.post_gcaptchav3(job.site_url, job.site_key, job.action)
elif isinstance(job, NocaptchaJob): elif isinstance(job, NocaptchaJob):
job.id = self.browser.post_nocaptcha(job.site_url, job.site_key) job.id = self.browser.post_nocaptcha(job.site_url, job.site_key)
elif isinstance(job, FuncaptchaJob): elif isinstance(job, FuncaptchaJob):
......
...@@ -17,6 +17,7 @@ except AttributeError: ...@@ -17,6 +17,7 @@ except AttributeError:
# can't create a subclass because of CapBank.iter_resources reimplementations: # can't create a subclass because of CapBank.iter_resources reimplementations:
# modules will import our subclass, but boobank will call iter_resources with the OLD class # modules will import our subclass, but boobank will call iter_resources with the OLD class
Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account') Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Loan._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)') Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)')
......
...@@ -42,7 +42,9 @@ from .pages.bank import ( ...@@ -42,7 +42,9 @@ from .pages.bank import (
AccountsPage as BankAccountsPage, CBTransactionsPage, TransactionsPage, AccountsPage as BankAccountsPage, CBTransactionsPage, TransactionsPage,
UnavailablePage, IbanPage, LifeInsuranceIframe, BoursePage, BankProfilePage, UnavailablePage, IbanPage, LifeInsuranceIframe, BoursePage, BankProfilePage,
) )
from .pages.wealth import AccountsPage as WealthAccountsPage, InvestmentPage, HistoryPage, ProfilePage from .pages.wealth import (
AccountsPage as WealthAccountsPage, InvestmentPage, HistoryPage, ProfilePage, AccountDetailsPage,
)
from .pages.transfer import ( from .pages.transfer import (
RecipientsPage, AddRecipientPage, ValidateTransferPage, RegisterTransferPage, RecipientsPage, AddRecipientPage, ValidateTransferPage, RegisterTransferPage,
ConfirmTransferPage, RecipientConfirmationPage, ConfirmTransferPage, RecipientConfirmationPage,
...@@ -217,6 +219,7 @@ class AXABanque(AXABrowser, StatesMixin): ...@@ -217,6 +219,7 @@ class AXABanque(AXABrowser, StatesMixin):
self.transactions.go() self.transactions.go()
self.cache['accs'] = accounts self.cache['accs'] = accounts
self.bank_accounts.go()
return self.cache['accs'] return self.cache['accs']
@need_login @need_login
...@@ -505,13 +508,14 @@ class AXABanque(AXABrowser, StatesMixin): ...@@ -505,13 +508,14 @@ class AXABanque(AXABrowser, StatesMixin):
class AXAAssurance(AXABrowser): class AXAAssurance(AXABrowser):
BASEURL = 'https://espaceclient.axa.fr' BASEURL = 'https://espaceclient.axa.fr'
accounts = URL('/accueil.html', WealthAccountsPage) accounts = URL(r'/accueil.html', WealthAccountsPage)
investment = URL('/content/ecc-popin-cards/savings/[^/]+/repartition', InvestmentPage) account_details = URL('.*accueil/savings/(\w+)/contract',
history = URL('.*accueil/savings/(\w+)/contract', r'https://espaceclient.axa.fr/#', AccountDetailsPage)
'https://espaceclient.axa.fr/#', HistoryPage) investment = URL(r'/content/ecc-popin-cards/savings/[^/]+/repartition', InvestmentPage)
documents = URL('https://espaceclient.axa.fr/content/espace-client/accueil/mes-documents/attestations-d-assurances.content-inner.din_CERTIFICATE.html', DocumentsPage) history = URL(r'/content/ecc-popin-cards/savings/savings/postsales.mawGetPostSalesOperations.json', HistoryPage)
download = URL('/content/ecc-popin-cards/technical/detailed/document.downloadPdf.html', documents = URL(r'https://espaceclient.axa.fr/content/espace-client/accueil/mes-documents/attestations-d-assurances.content-inner.din_CERTIFICATE.html', DocumentsPage)
'/content/ecc-popin-cards/technical/detailed/document/_jcr_content/', download = URL(r'/content/ecc-popin-cards/technical/detailed/document.downloadPdf.html',
r'/content/ecc-popin-cards/technical/detailed/document/_jcr_content/',
DownloadPage) DownloadPage)
profile = URL(r'/content/ecc-popin-cards/transverse/userprofile.content-inner.html\?_=\d+', ProfilePage) profile = URL(r'/content/ecc-popin-cards/transverse/userprofile.content-inner.html\?_=\d+', ProfilePage)
...@@ -560,18 +564,21 @@ class AXAAssurance(AXABrowser): ...@@ -560,18 +564,21 @@ class AXAAssurance(AXABrowser):
@need_login @need_login
def iter_history(self, account): def iter_history(self, account):
self.go_wealth_pages(account) ''' There is now an API for the accounts history, however transactions are not
pagination_url = self.page.get_pagination_url() sorted by date in the JSON. The website fetches 5 years of history maximum.
try: For some accounts, the access to the transactions JSON is not available yet. '''
self.location(pagination_url, params={'skip': 0}) params = {
except ClientError as e: 'startDate': (date.today() - relativedelta(years=2)).year,
assert e.response.status_code == 406 'endDate': date.today().year,
self.logger.info('not doing pagination for account %r, site seems broken', account) 'pid': account.id,
for tr in self.page.iter_history(no_pagination=True): }
yield tr self.history.go(params=params)
error_code = self.page.get_error_code()
if error_code:
self.logger.warning('Error when trying to access the history JSON, history will be skipped for this account.')
return return
for tr in self.page.iter_history(): for tr in sorted_transactions(self.page.iter_history()):
yield tr yield tr
def iter_coming(self, account): def iter_coming(self, account):
......
...@@ -17,6 +17,7 @@ except AttributeError: ...@@ -17,6 +17,7 @@ except AttributeError:
# can't create a subclass because of CapBank.iter_resources reimplementations: # can't create a subclass because of CapBank.iter_resources reimplementations:
# modules will import our subclass, but boobank will call iter_resources with the OLD class # modules will import our subclass, but boobank will call iter_resources with the OLD class
Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account') Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Loan._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)') Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)')
......
...@@ -82,6 +82,7 @@ class AccountsPage(LoggedPage, MyHTMLPage): ...@@ -82,6 +82,7 @@ class AccountsPage(LoggedPage, MyHTMLPage):
('livret', Account.TYPE_SAVINGS), ('livret', Account.TYPE_SAVINGS),
('ldd', Account.TYPE_SAVINGS), ('ldd', Account.TYPE_SAVINGS),
('pel', Account.TYPE_SAVINGS), ('pel', Account.TYPE_SAVINGS),
('cel', Account.TYPE_SAVINGS),
('pea', Account.TYPE_PEA), ('pea', Account.TYPE_PEA),
('titres', Account.TYPE_MARKET), ('titres', Account.TYPE_MARKET),
('valorisation', Account.TYPE_MARKET), ('valorisation', Account.TYPE_MARKET),
......
...@@ -17,6 +17,7 @@ except AttributeError: ...@@ -17,6 +17,7 @@ except AttributeError:
# can't create a subclass because of CapBank.iter_resources reimplementations: # can't create a subclass because of CapBank.iter_resources reimplementations:
# modules will import our subclass, but boobank will call iter_resources with the OLD class # modules will import our subclass, but boobank will call iter_resources with the OLD class
Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account') Account._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Loan._fields['ownership'] = StringField('Relationship between the credentials owner (PSU) and the account')
Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)') Transaction._fields['bdate'] = DateField('Bank date, when the transaction appear on website (usually extracted from column date)')
......
...@@ -17,28 +17,29 @@ ...@@ -17,28 +17,29 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>. # along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import re import re
from weboob.browser.pages import HTMLPage, LoggedPage, pagination from decimal import Decimal
from weboob.browser.elements import ListElement, ItemElement, method, TableElement from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage
from weboob.browser.elements import ListElement, DictElement, ItemElement, method, TableElement
from .compat.weboob_browser_filters_standard import ( from .compat.weboob_browser_filters_standard import (
Async, AsyncLoad, CleanDecimal, CleanText, Currency, Date, Eval, Field, Lower, MapIn, QueryValue, Regexp, CleanDecimal, CleanText, Currency, Date, Eval, Field, Lower, MapIn, QueryValue, Regexp,
) )
from weboob.browser.filters.html import Attr, Link, TableCell from weboob.browser.filters.html import Attr, Link, TableCell
from weboob.browser.filters.json import Dict
from .compat.weboob_capabilities_bank import Account, Investment from .compat.weboob_capabilities_bank import Account, Investment
from weboob.capabilities.profile import Person from weboob.capabilities.profile import Person
from weboob.capabilities.base import NotAvailable, NotLoaded from weboob.capabilities.base import NotAvailable, NotLoaded
from weboob.tools.capabilities.bank.transactions import FrenchTransaction from weboob.tools.capabilities.bank.transactions import FrenchTransaction
def MyDecimal(*args, **kwargs): def float_to_decimal(f):
kwargs.update(replace_dots=True, default=NotAvailable) return Decimal(str(f))
return CleanDecimal(*args, **kwargs)
class AccountsPage(LoggedPage, HTMLPage): class AccountsPage(LoggedPage, HTMLPage):
@method @method
class iter_accounts(ListElement): class iter_accounts(ListElement):
item_xpath = '//div[contains(@data-route, "/savings/")]' item_xpath = '//div[contains(@data-route, "/savings/")]'
...@@ -55,10 +56,10 @@ class AccountsPage(LoggedPage, HTMLPage): ...@@ -55,10 +56,10 @@ class AccountsPage(LoggedPage, HTMLPage):
condition = lambda self: Field('balance')(self) is not NotAvailable condition = lambda self: Field('balance')(self) is not NotAvailable
obj_id = Regexp(CleanText('.//span[has-class("small-title")]'), '(\d+)') obj_id = Regexp(CleanText('.//span[has-class("small-title")]'), r'([\d/]+)')
obj_label = CleanText('.//h3[has-class("card-title")]') obj_label = CleanText('.//h3[has-class("card-title")]')
obj_balance = MyDecimal('.//p[has-class("amount-card")]') obj_balance = CleanDecimal.French('.//p[has-class("amount-card")]')
obj_valuation_diff = MyDecimal('.//p[@class="performance"]') obj_valuation_diff = CleanDecimal.French('.//p[@class="performance"]', default=NotAvailable)
def obj_url(self): def obj_url(self):
url = Attr('.', 'data-route')(self) url = Attr('.', 'data-route')(self)
...@@ -154,66 +155,40 @@ class InvestmentPage(LoggedPage, HTMLPage): ...@@ -154,66 +155,40 @@ class InvestmentPage(LoggedPage, HTMLPage):
return bool(self.doc.xpath(u'//th[contains(text(), "Valeur de la part")]')) return bool(self.doc.xpath(u'//th[contains(text(), "Valeur de la part")]'))
class Transaction(FrenchTransaction): class AccountDetailsPage(LoggedPage, HTMLPage):
PATTERNS = [(re.compile(u'^(?P<text>souscription.*)'), FrenchTransaction.TYPE_DEPOSIT),
(re.compile(u'^(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
]
class HistoryPage(LoggedPage, HTMLPage):
def build_doc(self, content):
# we got empty pages at end of pagination
if not content.strip():
content = b"<html></html>"
return super(HistoryPage, self).build_doc(content)
def get_account_url(self, url): def get_account_url(self, url):
return Attr(u'//a[@href="%s"]' % url, 'data-target')(self.doc) return Attr('//a[@href="%s"]' % url, 'data-target')(self.doc)
def get_investment_url(self): def get_investment_url(self):
return Attr('//div[has-class("card-distribution")]', 'data-url', default=None)(self.doc) return Attr('//div[has-class("card-distribution")]', 'data-url', default=None)(self.doc)
def get_pagination_url(self):
return Attr('//div[contains(@class, "default")][@data-module-card-list--current-page]', 'data-module-card-list--url')(self.doc)
@method
class get_investments(ListElement):
item_xpath = '//div[@class="white-bg"][.//strong[contains(text(), "support")]]/following-sibling::div'
class item(ItemElement): class Transaction(FrenchTransaction):
klass = Investment PATTERNS = [
(re.compile('^(?P<text>souscription.*)'), FrenchTransaction.TYPE_DEPOSIT),
(re.compile('^(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
]
obj_label = CleanText('.//div[has-class("t-data__label")]')
obj_valuation = MyDecimal('.//div[has-class("t-data__amount") and has-class("desktop")]')
obj_portfolio_share = Eval(lambda x: x / 100, CleanDecimal('.//div[has-class("t-data__amount_label")]'))
@pagination class HistoryPage(LoggedPage, JsonPage):
@method @method
class iter_history(ListElement): class iter_history(DictElement):