pax_global_header 0000666 0000000 0000000 00000000064 14575653726 0014536 g ustar 00root root 0000000 0000000 52 comment=5f3d558793b537a74480241ac6981479f5938cd3
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-axabanque/ 0000775 0000000 0000000 00000000000 14575653726 0024007 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-axabanque/modules/ 0000775 0000000 0000000 00000000000 14575653726 0025457 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-axabanque/modules/axabanque/ 0000775 0000000 0000000 00000000000 14575653726 0027424 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-axabanque/modules/axabanque/__init__.py0000664 0000000 0000000 00000001531 14575653726 0031535 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# flake8: compatible
# Copyright(C) 2013 Romain Bignon
#
# This file is part of a woob module.
#
# This woob module 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.
#
# This woob 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this woob module. If not, see .
from .module import AXABanqueModule
__all__ = ['AXABanqueModule']
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-axabanque/modules/axabanque/browser.py 0000664 0000000 0000000 00000067520 14575653726 0031473 0 ustar 00root root 0000000 0000000 # Copyright(C) 2016 Edouard Lambert
#
# This file is part of a woob module.
#
# This woob module 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.
#
# This woob 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this woob module. If not, see .
# flake8: compatible
import re
import random
from datetime import date
from base64 import b64encode
from hashlib import sha256
import os
from urllib.parse import urljoin, urlparse
from dateutil.relativedelta import relativedelta
from woob.browser import LoginBrowser, URL, need_login
from woob.browser.browsers import OAuth2PKCEMixin
from woob.browser.exceptions import BrowserUnavailable, ClientError, ServerError
from woob.browser.filters.standard import QueryValue
from woob.browser.switch import SiteSwitch
from woob.capabilities.bank import Account
from woob.exceptions import (
BrowserPasswordExpired, BrowserIncorrectPassword, ActionNeeded, ActionType, BrowserUserBanned,
)
from woob.tools.capabilities.bank.transactions import sorted_transactions
from woob.tools.decorators import retry
from woob_modules.allianzbanque.browser import AllianzbanqueBrowser
from .pages.bank import AccountsPage
from .pages.login import (
LoginPage, ChangepasswordPage, PredisconnectedPage, DeniedPage,
AccountSpaceLogin, ErrorPage, AuthorizePage, InfiniteLoopPage, LoginEndPage,
)
from .pages.wealth import (
AccountsPage as WealthAccountsPage, AccountDetailsPage, InvestmentPage,
InvestmentMonAxaPage, HistoryPage, HistoryInvestmentsPage, ProfilePage,
PerformanceMonAxaPage, InvestmentJsonPage, AccessBoursePage, FormHistoryPage,
BourseAccountsPage, WealthHistoryPage, NewInvestmentPage, InsuranceAccountsBouncerPage,
HomePage, ClearSessionPage, InvestmentErrorPage, OutremerProfilePage,
)
from .pages.document import SubscriptionsPage, DocumentsPage
WEALTH_ACCOUNTS = (
Account.TYPE_LIFE_INSURANCE, Account.TYPE_MADELIN, Account.TYPE_PER, Account.TYPE_PERP,
)
class AXAOldLoginBrowser(OAuth2PKCEMixin, LoginBrowser):
ACCESS_TOKEN_URI = 'https://connect.axa.fr/connect/token'
SCOPE = 'openid profile phone email openid api-client https://connect.axa.fr/individualOrProfessional.all https://connect.axa.fr/collectiveHealth.all https://connect.axa.fr/bank.all axa-fr-salary-saving eb-fr-collective fr-gfp https://connect.axa.fr/auth/pema'
# Login
login = URL(r'https://connect.axa.fr/api/identity/auth', LoginPage)
password = URL(r'https://connect.axa.fr/#/changebankpassword', ChangepasswordPage)
predisconnected = URL(
r'https://www.axa.fr/axa-predisconnect.html',
r'https://www.axa.fr/axa-postmaw-predisconnect.html',
PredisconnectedPage
)
authorize = URL(r'https://connect.axa.fr/connect/authorize', AuthorizePage)
denied = URL(r'https://connect.axa.fr/Account/AccessDenied', DeniedPage)
account_space_login = URL(r'https://connect.axa.fr/api/accountspace', AccountSpaceLogin)
errors = URL(
r'https://espaceclient.axa.fr/content/ecc-public/accueil-axa-connect/_jcr_content/par/text.html',
r'https://espaceclient.axa.fr/content/ecc-public/errors/500.html',
ErrorPage
)
login_end = URL(r'https://espaceclient.axa.fr/$', LoginEndPage)
infinite_redirect = URL(
r'http[s]?://www.axabanque.fr/webapp/axabanque/jsp(/erreur)?/[\d\.:]+/webapp/axabanque/jsp/erreur/erreurBanque',
# ex: 'http://www.axabanque.fr/webapp/axabanque/jsp/172.25.100.12:80/webapp/axabanque/jsp/erreur/erreurBanque.faces'
InfiniteLoopPage
)
def __init__(self, config, username, password, *args, **kwargs):
super(AXAOldLoginBrowser, self).__init__(username, password, *args, **kwargs)
# Not sure about this one, might be needed to generate it instead of hardcoding it
self.client_id = 'ef65ad31-82e2-4131-99fc-f2e3504fd02c'
self.oauth_state = os.urandom(5).hex()
self.redirect_uri = 'https://espaceclient.axa.fr/silent-redirection.html'
def do_login(self):
# Due to the website change, login changed too.
# This is for avoiding to log-in with the wrong login
if self.username.isdigit() and len(self.username) > 7:
raise BrowserPasswordExpired()
self.account_space_login.go()
password_message = self.page.get_password_information_message()
if 'votre code confidentiel doit être modifié' in password_message and self.password.isdigit():
raise BrowserPasswordExpired(
locale='fr-FR',
message=password_message
)
error_message = self.page.get_error_message()
if error_message:
is_website_unavailable = re.search(
"Veuillez nous excuser pour la gêne occasionnée"
+ "|votre espace client est temporairement indisponible",
error_message
)
if is_website_unavailable:
raise BrowserUnavailable(error_message)
if self.page.get_error_link():
# Go on information page to get possible error message
self.location(self.page.get_error_link())
login_data = {
'email': self.username,
'password': self.password,
'rememberIdenfiant': False,
}
self.location('https://connect.axa.fr')
try:
self.login.go(
json=login_data,
headers={'X-XSRF-TOKEN': self.session.cookies['XSRF-TOKEN']}
)
except ClientError as err:
response = err.response
if response.status_code == 400:
if 'INVALID_CREDENTIAL' in response.text:
raise BrowserIncorrectPassword()
if 'COMMON_ERROR_LOCK_ACCOUNT' in response.text:
raise BrowserUserBanned()
raise
if self.page.check_error():
raise BrowserIncorrectPassword()
if self.page.password_expired():
raise BrowserPasswordExpired()
url = self.page.get_url()
if 'bank-otp' in url:
# The SCA is Cross-Browser so the user can do the SMS validation on the website
# and then try to synchronize the connection again.
raise ActionNeeded(
locale="fr-FR", message="Vous devez réaliser la double authentification sur le portail internet",
action_type=ActionType.PERFORM_MFA,
)
# home page to finish login
self.location('https://espaceclient.axa.fr/', allow_redirects=False)
for _ in range(13):
# When trying to reach home we are normally redirected a few times.
# But very rarely we are redirected 13 times before entering
# an infinite redirection loop between 'infinite_redirect'
# url to another. Need to try later.
location = self.response.headers.get('location')
if not location:
break
self.location(location, allow_redirects=False)
if self.infinite_redirect.is_here():
raise BrowserUnavailable()
params = self.build_authorization_parameters()
params.update({
'response_type': 'code',
'scope': self.SCOPE,
'prompt': 'none',
})
self.authorize.go(params=params)
self.request_access_token(self.url)
class AXANewLoginBrowser(AllianzbanqueBrowser):
BASEURL = 'https://api-banque.axa.fr'
redirect_uri = 'https://banque.axa.fr/auth/checkuser'
error_uri = 'https://banque.axa.fr/auth/errorauthn'
arkea = 'AB' # Needed for the X-ARKEA-EFS header
arkea_si = '0AB'
arkea_client_id = 'O7v09LGq4zJsi5BWfuAGFK6KGLoX3QVh' # Hardcoded in app.js, line 33808
original_site = 'https://banque.axa.fr'
def code_challenge(self):
"""Generate a code challenge needed to go through the authorize end point
and get a session id.
The process to generate this code_challenge:
- Generate a 128 length string from which characters are randomly choosen
among the base string.
- b64encode this string
- Replace '=', '+', '/' respectively with these '', '-' and '_'.
Found in domi-auth-fat.js at line 39501"""
base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
code_challenge = b64encode(
''.join(random.choices(base, k=128)).encode('utf-8')
).decode('utf-8').replace('=', '').replace('+', '-').replace('/', '_')
return code_challenge
def auth_state(self):
"""Generate a state needed to go through the authorize end point
and get a session id.
Found in domi-auth-fat.js at line 39518"""
base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
state = 'auth_' + ''.join(random.choices(base, k=25))
return state
def code_verifier(self, code_challenge):
"""Generate a code verifier that will have to match the sha256 of the code_challenge
on the server side.
Found in domi-auth-fat.js at line 39509"""
digest = sha256(code_challenge.encode('utf-8')).digest()
code_verifier = b64encode(digest)
return code_verifier.decode()
def get_pkce_codes(self):
"""Override parent because Axa did things well
and did not use verifier for code_challenge and vice versa."""
code_challenge = self.code_challenge()
return self.code_verifier(code_challenge), code_challenge
def build_authorization_uri_params(self):
params = super(AXANewLoginBrowser, self).build_authorization_uri_params()
params['state'] = self.auth_state()
return params
def setup_space_after_login(self):
"""Override from parent because AXABanque has no Pro/Part space."""
pass
class AXABanqueBrowser(AXANewLoginBrowser):
STATE_DURATION = 10
home = URL(r'https://espaceclient-connect.axa.fr/', HomePage)
insurance_accounts_bouncer = URL(
r'/oidc/init-sso-axf',
InsuranceAccountsBouncerPage,
)
insurances = URL(
r'https://(?P.*).axa.fr/content/(?P.*)/accueil.content-inner.html',
WealthAccountsPage
)
clear_session = URL(r'https://connect.axa.fr/identity/clear-session', ClearSessionPage)
authorize = URL(r'/oauth/authorize', AuthorizePage)
accounts = URL(r'/distri-account-api/api/v1/persons/me/accounts', AccountsPage)
transactions_comings = URL(
r'/distri-account-api/api/v1/persons/me/accounts/(?P[A-Z0-9]{10})/upcoming-transactions',
AccountsPage
)
subscriptions = URL(r'/distri-account-api/api/v1/customers/me/accounts', SubscriptionsPage)
documents = URL(r'/documentapi/api/v2/vaults/(?P.*)/documents$', DocumentsPage)
document_pdf = URL(r'/documentapi/api/v2/vaults/(?P.*)/documents/(?P.*)/file')
def locate_browser(self, state):
# access_token lasts 180 seconds according to the JSON in which
# we get it but when browsing the website, it is in fact valid
# for about 10 minutes. Anyway, this is too short to store it so
# to avoid 401 on some URLs, just skip locate_browser and relog.
pass
def __init__(self, *args, **kwargs):
super(AXABanqueBrowser, self).__init__(*args, **kwargs)
self.axa_assurance_base_url = None
self.axa_assurance_url_path = None
self.is_coming_from_axa_bank = None
self.__states__ += (
'axa_assurance_base_url', 'axa_assurance_url_path', 'is_coming_from_axa_bank',
)
@need_login
def iter_accounts(self):
self.accounts.go(params={'types': 'CHECKING,SAVING'})
for account in self.page.iter_accounts():
self.balances.go(account_id=account.id)
self.page.fill_balance(account)
date_to = (date.today() + relativedelta(months=1)).strftime('%Y-%m-%dT00:00:00.000Z')
go_balances_comings = retry(ServerError, tries=5)(self.balances_comings.go)
go_balances_comings(account_id=account.id, params={'dateTo': date_to})
self.page.fill_coming(account)
yield account
if self.go_to_insurance_accounts():
for acc in self.page.iter_accounts():
yield acc
def go_to_outremer_or_metropolitan(self, axa_assurance_url_path, url_domain):
self.axa_assurance_url_path = axa_assurance_url_path # This will be needed when switching browser
self.insurances.go(url_domain=url_domain, url_path=self.axa_assurance_url_path)
# Going to insurances page sometimes triggers a redirection to the Axa Assurance homepage
# instead of the insurances pages. Requesting the page a second time if that's the case.
if 'espaceclient.axa.fr' in self.url:
self.insurances.go(url_domain=url_domain, url_path=self.axa_assurance_url_path)
def go_to_insurance_accounts(self):
self.insurance_accounts_bouncer.go()
# First time we go to insurance_accounts_bouncer, we're always correctly
# redirected to either outremer or classic space. If we ever have to load
# this URL again during the same session, redirection process will stop
# on clear_session URL that contains the needed redirection in its parameters
# but won't call it automatically. Calling it manually makes the redirection
# process be the same as when we first load insurance_accounts_bouncer.
if self.clear_session.is_here():
return_url = QueryValue(None, 'returnUrl').filter(self.url)
self.location(return_url)
# In case the user has only a bank space, but no insurance one.
if self.home.is_here():
return False
self.axa_assurance_base_url = self.url
if urlparse(self.url).netloc == 'outremer.axa.fr':
self.go_to_outremer_or_metropolitan('outremer-espace-client', 'outremer')
else:
self.go_to_outremer_or_metropolitan('espace-client', 'espaceclient')
# If we're here, then we are going to switch to insurance browser later and this information
# will be needed to distinguish AXABanqueBrowser accounts that also have insurances from
# AXAAssuranceBrowser that only have insurances and for which the login is different
self.is_coming_from_axa_bank = True
return True
@need_login
def iter_history(self, account):
# History for bank account is well managed by parents
if account.type in (Account.TYPE_CHECKING, Account.TYPE_SAVINGS):
return super(AXABanqueBrowser, self).iter_history(account)
# On the other hand for wealth products, there's a dedicated space for them
# Which has nothing to do with parents modules
elif account.type in (Account.TYPE_MARKET, Account.TYPE_PEA):
raise SiteSwitch('bourse')
elif account.type in WEALTH_ACCOUNTS:
raise SiteSwitch('insurance')
else:
return []
@need_login
def iter_investment(self, account):
if account.type == Account.TYPE_MARKET:
raise SiteSwitch('bourse')
elif account.type in WEALTH_ACCOUNTS:
raise SiteSwitch('insurance')
return []
@need_login
def get_subscription_list(self):
params = {
'types': 'CHECKING',
'roles': 'TIT,COT',
}
self.subscriptions.go(params=params)
return self.page.iter_subscriptions()
@need_login
def iter_documents(self, subscription):
self.documents.go(contract_id=subscription._contract_id)
return self.page.iter_documents(subid=subscription.id, contract_id=subscription._contract_id)
@need_login
def download_document(self, document):
params = {'flattenDoc': False}
return self.open(document.url, params=params).content
class AXABourseBrowser(AXABanqueBrowser):
BASEURL = 'https://bourse.axa.fr/'
request_access = URL(r'https://api-banque.axa.fr/sso-domifront/cypher\?service=bourse', AccessBoursePage)
bourse = URL(r'/receiver')
accounts = URL(r'/secure_main/accounts_list.html\?navId=SYN', BourseAccountsPage)
history = URL(r'/secure_account/selectedAccountMovements.html', WealthHistoryPage)
form_history = URL(r'/secure_ajax/accountMovementsResult.html', FormHistoryPage)
investments = URL(r'/secure_account/selectedAccountDetail.html', NewInvestmentPage)
def do_login(self):
"""The way we logging in the bourse space is a little bit special.
We access it from the AxaBanque user space and by requesting access. Thus
we obtain a token that is passed in parameter of the receiver endpoint."""
self.request_access.go()
cypher = self.page.get_cypher()
self.bourse.go(params={'cypher': cypher})
@need_login
def iter_history(self, account):
if account.type in (Account.TYPE_CHECKING, Account.TYPE_SAVINGS):
raise SiteSwitch('main')
elif account.type in WEALTH_ACCOUNTS:
raise SiteSwitch('insurance')
"""History of operations is quite a mess.
We need to deal with a double pagination:
First, we have to select a range of time through the history form.
This is our first pagination, we iterate over years, over and over, until there's no more transactions.
Second, for each range of year, AxaBanque pagine transactions ten by ten
This is our second pagination. We iterate over each pack of ten transactions
until we've reached the last page.
"""
# First, if the wealth account is a market/PEA one
# We need to collect related investments
# So we can match transactions with them
investments = self.iter_investment(account)
# Mapping invest with their ISIN code so we won't
# have to double loop over transactions and then investments
# to do the match
investments = {invest.code: [invest] for invest in investments}
self.accounts.stay_or_go()
self.history.go(
params={'cipher': self.page.get_cipher(account.number)},
)
end_date = date.today()
begin_date = end_date - relativedelta(years=1)
transactions = []
while True:
self.form_history.go(
data={
'siteLanguage': 'fr',
'beginDate': begin_date.strftime('%d/%m/%Y'),
'endDate': end_date.strftime('%d/%m/%Y'),
},
)
if not self.page.has_more_transactions():
break
end_date -= relativedelta(years=1)
begin_date -= relativedelta(years=1)
transactions.extend(self.page.iter_history(investments=investments))
return transactions
@need_login
def iter_investment(self, account):
if account.type in (Account.TYPE_CHECKING, Account.TYPE_SAVINGS):
raise SiteSwitch('main')
elif account.type in WEALTH_ACCOUNTS:
raise SiteSwitch('insurance')
if account.balance > 0:
self.accounts.stay_or_go()
self.investments.go(
params={'cipher': self.page.get_cipher(account.number)},
)
return self.page.iter_investments()
return []
class AXAAssuranceBrowser(AXAOldLoginBrowser):
BASEURL = 'https://espaceclient.axa.fr' # Default BASEURL for AXAAssuranceBrowser accounts only
accounts = URL(
r'https://apis.axa.fr/ecv2/transverse/home-dashboard/policies',
WealthAccountsPage,
)
history = URL(
r'/accueil/savings/savings/contract/_jcr_content.eccGetSavingsOperations.json',
HistoryPage,
)
history_investments = URL(
r'/accueil/savings/savings/contract/_jcr_content.eccGetSavingOperationDetail.json',
HistoryInvestmentsPage,
)
details = URL(
r'/content/espace-client/accueil/savings/retirement/contract.content-inner.pid_.*.aid_.*.html',
r'/#/savings/(?P.*)',
AccountDetailsPage,
)
investment_error = URL(
r'/public/errors/500.html',
InvestmentErrorPage,
)
investment = URL(
r'/content/ecc-popin-cards/savings/[^/]+/repartition',
r'/popin-cards/savings/[^/]+/repartition/.*',
r'/content/ecc-popin-cards/savings/retirement/repartition/_jcr_content/par.pid_.*.aid_.*.html',
InvestmentPage,
)
investment_json = URL(
r'/content/(?P.*)/accueil/_jcr_content.savingsDistribution.json',
InvestmentJsonPage,
)
investment_monaxa = URL(r'https://monaxaweb-gp.axa.fr/MonAxa/Contrat/', InvestmentMonAxaPage)
performance_monaxa = URL(r'https://monaxaweb-gp.axa.fr/MonAxa/ContratPerformance/', PerformanceMonAxaPage)
profile = URL(
r'/content/ecc-popin-cards/transverse/userprofile.content-inner.html\?_=\d+',
ProfilePage,
)
outremer_profile = URL(
r'/content/outremer-espace-client/popin-cards/transverse/userprofile.content-inner.html\?_=\d+',
OutremerProfilePage,
)
def __init__(self, *args, **kwargs):
super(AXAAssuranceBrowser, self).__init__(*args, **kwargs)
self.axa_assurance_url_path = 'espace-client' # Default path for URLs for AXAAssuranceBrowser accounts only
self.is_coming_from_axa_bank = None # Default for AXAassuranceBrowser accounts only
def set_base_url(self):
# BASEURL can't be defined in the __init__ because SwitchingBrowser set the values from
# the __states__ as attributes for the second browser only after that second browser has been instanciated.
# BASEURL can't be set as a property too because SwitchingBrowser will directly call the method it has
# been given in the first browser, e.g. if we came from browser 1 with iter_investment, then
# we have no choice but to call set_base_url() at the beginning of the method in browser 2.
if self.is_coming_from_axa_bank:
self.BASEURL = self.axa_assurance_base_url
def go_wealth_pages(self, account):
# Sometimes, a random page about an error 500 can be returned
# with a status_code 200 and website itself says it might work
# after reloading the page
location = retry(ClientError)(self.location)
location(urljoin(self.BASEURL, account.url))
if self.investment_error.is_here(): # If retrying failed
raise BrowserUnavailable()
@need_login
def iter_accounts(self):
self.accounts.go()
for acc in self.page.iter_accounts():
self.location(urljoin(self.BASEURL, acc.url))
acc.url = self.page.get_real_account_url()
yield acc
# Logged property is needed when switching from the main browser
# to the insurance browser.
@property
def logged(self):
return bool(self.session.headers.get('Authorization'))
@need_login
def iter_investment_espaceclient(self, account):
invests = []
portfolio_page = self.page
detailed_view = self.page.detailed_view()
if detailed_view:
self.location(detailed_view)
invests.extend(self.page.iter_investment(currency=account.currency))
for inv in portfolio_page.iter_investment(currency=account.currency):
i = [i2 for i2 in invests if
(i2.valuation == inv.valuation and i2.label == inv.label)]
assert len(i) in (0, 1)
if i:
i[0].portfolio_share = inv.portfolio_share
else:
invests.append(inv)
return invests
@need_login
def iter_investment_monaxa(self, account):
# Try to fetch a URL to 'monaxaweb-gp.axa.fr'
invests = list(self.page.iter_investment())
performance_url = self.page.get_performance_url()
if performance_url:
self.location(performance_url)
for inv in invests:
self.page.fill_investment(obj=inv)
# return to espaceclient.axa.fr or outremer.axa.fr
self.accounts.go(url_path=self.axa_assurance_url_path)
return invests
@need_login
def iter_investment(self, account):
if account.type in (Account.TYPE_CHECKING, Account.TYPE_SAVINGS):
return
elif account.type == Account.TYPE_MARKET:
raise SiteSwitch('bourse')
self.set_base_url()
self.go_wealth_pages(account)
self.investment_json.go(
url_path=self.axa_assurance_url_path,
params={'pid': account._pid}
)
if self.investment_json.is_here() and not self.page.is_error():
return self.page.iter_investments()
self.go_wealth_pages(account)
investment_url = self.page.get_investment_url()
if investment_url:
self.location(investment_url)
return self.iter_investment_espaceclient(account)
iframe_url = self.page.get_iframe_url()
if iframe_url:
# Set correct Referer to avoid 302 followed by 406 errors (Not Acceptable)
base_url = self.BASEURL
if urlparse(self.page.url).netloc == 'outremer.axa.fr':
base_url = self.OUTREMER_BASEURL
headers = {'Referer': f'{base_url}/accueil.html'}
self.location(iframe_url, headers=headers)
return self.iter_investment_monaxa(account)
# No data available for this account.
self.logger.warning('No investment URL available for account %s, investments cannot be retrieved.', account.id)
return []
@need_login
def iter_history(self, account):
if account.type in (Account.TYPE_CHECKING, Account.TYPE_SAVINGS):
raise SiteSwitch('main')
elif account.type in (Account.TYPE_MARKET, Account.TYPE_PEA):
raise SiteSwitch('bourse')
self.set_base_url()
'''
Transactions are available 10 by 10 in a JSON.
To access it, we need the account 'pid' and to increment
'skip' for each transaction page until the JSON is empty.
However, transactions are not always in the chronological order.
'''
self.go_wealth_pages(account)
skip = 0
if not account._pid:
self.logger.warning('No pid available for account %s, transactions cannot be retrieved.', account.id)
return
transactions = []
self.go_to_transactions(account._pid, skip)
# Pagination:
while self.page.has_operations():
for tr in self.page.iter_history():
transactions.append(tr)
skip += 10
self.go_to_transactions(account._pid, skip)
for tr in sorted_transactions(transactions):
# Get investments for each transaction
params = {
'oid': tr._oid,
'pid': account._pid,
}
self.history_investments.go(params=params)
if self.page.has_investments():
tr.investments = list(self.page.iter_transaction_investments())
else:
tr.investments = []
yield tr
def go_to_transactions(self, pid, skip):
params = {
'pid': pid,
'skip': skip,
}
self.history.go(params=params)
def iter_coming(self, account):
raise NotImplementedError()
def get_subscription_list(self):
raise NotImplementedError()
@need_login
def get_profile(self):
if 'outremer' in self.BASEURL:
self.outremer_profile.go()
else:
self.profile.go()
return self.page.get_profile()
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-axabanque/modules/axabanque/favicon.png0000664 0000000 0000000 00000004345 14575653726 0031565 0 ustar 00root root 0000000 0000000 PNG
IHDR @ @ iq bKGD C pHYs
B(x tIME :.d rIDATxyTɢ,hT(Ĉ=jc@p?u͉ъӜxĈZڪK$5J"&F1U4*
2>cUYqdnּSY5κ|1Ɉ ^,Nͬs*1ewu9?[uV՜erGGMƠgc/JCI :C/۔}Sy?BaL'WU,#=@}}ohws6
'wʮ|k#:)%hKٔNO!)ۊ&{#`~z3bi5w)+*d\O[
.C@H_6ZB-lNGkU"D5S=F#IYe v\u
5oVhK KM"a,OeA~l ̚<lDa ܌Xz)hYN\$`qZ4s3bQ'.yG8m|Ӣ {o'sٺuP뼦y\ŷ}FE;C0?=F륮Vk0&3ζN$uIB9[o}v%@cSK6ZhSF+DU