pax_global_header 0000666 0000000 0000000 00000000064 14575653726 0014536 g ustar 00root root 0000000 0000000 52 comment=5f3d558793b537a74480241ac6981479f5938cd3
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-amundi/ 0000775 0000000 0000000 00000000000 14575653726 0023317 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-amundi/modules/ 0000775 0000000 0000000 00000000000 14575653726 0024767 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-amundi/modules/amundi/ 0000775 0000000 0000000 00000000000 14575653726 0026244 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-amundi/modules/amundi/__init__.py 0000664 0000000 0000000 00000001500 14575653726 0030351 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 James GALT
#
# 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 AmundiModule
__all__ = ['AmundiModule']
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-amundi/modules/amundi/browser.py 0000664 0000000 0000000 00000066203 14575653726 0030310 0 ustar 00root root 0000000 0000000 # Copyright(C) 2016 James GALT
# flake8: compatible
#
# 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 .
import time
from uuid import uuid4
import re
from woob.browser import URL, need_login
from woob.browser.mfa import TwoFactorBrowser
from woob.capabilities.captcha import RecaptchaV2Question
from woob.exceptions import (
AppValidation, AppValidationCancelled, AppValidationExpired, BrowserIncorrectPassword,
BrowserUserBanned, NotImplementedWebsite,
)
from woob.browser.exceptions import (
ClientError, ServerError, BrowserHTTPNotFound,
)
from woob.capabilities.base import empty, NotAvailable
from woob.capabilities.bank import Account
from woob.tools.capabilities.bank.transactions import sorted_transactions
from woob.tools.captcha.virtkeyboard import VirtKeyboardError
from woob.tools.decorators import retry
from .pages import (
AuthenticateFailsPage, ConfigPage, LoginPage, AccountsPage, AccountHistoryPage,
AmundiInvestmentsPage, AllianzInvestmentPage, EEInvestmentPage, InvestmentPerformancePage,
InvestmentDetailPage, EEProductInvestmentPage, ESAccountsPage, EresInvestmentPage, CprInvestmentPage,
CprPerformancePage, BNPInvestmentPage, BNPInvestmentApiPage, AxaInvestmentPage, AxaInvestmentApiPage,
EpsensInvestmentPage, EcofiInvestmentPage, MFAStatusPage, SGGestionInvestmentPage,
SGGestionPerformancePage, OlisnetInvestmentPage,
)
class AmundiBrowser(TwoFactorBrowser):
TIMEOUT = 120.0
HAS_CREDENTIALS_ONLY = True
STATE_DURATION = 10
login = URL(r'public/login/virtualKeyboard', LoginPage)
mfa_status = URL(r'/public/individu/push\?jti=(?P)', MFAStatusPage)
config_page = URL(r'public/config', ConfigPage)
authenticate_fails = URL(r'public/authenticateFails', AuthenticateFailsPage)
accounts = URL(r'api/individu/dispositifsMulti\?flagUrlFicheFonds=true&codeLangueIso2=fr', AccountsPage)
account_history = URL(
r'api/individu/operations\?valeurExterne=false&filtreStatutModeExclusion=false&statut=CPTA',
AccountHistoryPage,
)
# Amundi.fr investments
amundi_investments = URL(r'https://www.amundi.fr/fr_part/product/view', AmundiInvestmentsPage)
# EEAmundi browser investments
ee_investments = URL(
r'https://www.amundi-ee.com/part/home_fp&partner=PACTEO_SYS',
r'https://funds.amundi-ee.com/productsheet/open/',
EEInvestmentPage,
)
performance_details = URL(r'https://(.*)/ezjscore/call(.*)_tab_2', InvestmentPerformancePage)
investment_details = URL(r'https://(.*)/ezjscore/call(.*)_tab_5', InvestmentDetailPage)
# EEAmundi product investments
ee_product_investments = URL(r'https://www.amundi-ee.com/product', EEProductInvestmentPage)
# Allianz GI investments
allianz_investments = URL(r'https://fr.allianzgi.com', AllianzInvestmentPage)
# Eres investments
eres_investments = URL(r'https://www.eres-group.com/eres/new_fiche_fonds.php', EresInvestmentPage)
# CPR asset management investments
cpr_investments = URL(r'https://www.cpr-am.fr/particuliers/product/view', CprInvestmentPage)
cpr_performance = URL(r'https://www.cpr-am.fr/particuliers/ezjscore', CprPerformancePage)
# BNP Paribas Epargne Retraite Entreprises
bnp_investments = URL(
r'https://www.epargne-retraite-entreprises.bnpparibas.com/entreprises/fonds',
r'https://www.epargne-retraite-entreprises.bnpparibas.com/epargnants/fonds',
BNPInvestmentPage,
)
bnp_investment_api = URL(
r'https://www.epargne-retraite-entreprises.bnpparibas.com/api2/funds/overview/(?P.*)',
BNPInvestmentApiPage,
)
# AXA investments
axa_investments = URL(r'https://(.*).axa-im.fr/fonds', AxaInvestmentPage)
axa_inv_api_redirection = URL(
r'https://(?P.*).axa-im.fr/o/fundscenter/api/funds/detail/header/fr_FR/(?P.*)',
AxaInvestmentApiPage,
)
axa_inv_api = URL(
r'https://(?P.*).axa-im.fr/o/fundscenter/api/funds/detail/(?P.*)/performance/table/cumulative/fr_FR',
AxaInvestmentApiPage,
)
# Epsens investments
epsens_investments = URL(r'https://www.epsens.com/information-financiere', EpsensInvestmentPage)
# Ecofi investments
ecofi_investments = URL(r'http://www.ecofi.fr/fr/fonds/dynamis-solidaire', EcofiInvestmentPage)
# Société Générale gestion investments
sg_gestion_investments = URL(
r'https://www.societegeneralegestion.fr/psSGGestionEntr/productsheet/view/idvm',
SGGestionInvestmentPage,
)
sg_gestion_performance = URL(
r'https://www.societegeneralegestion.fr/psSGGestionEntr/ezjscore/call',
SGGestionPerformancePage,
)
# olisnet investments
olisnet_investments = URL(r'https://ims.olisnet.com/extranet/(?P).*', OlisnetInvestmentPage)
def __init__(self, config, *args, **kwargs):
super().__init__(config, *args, **kwargs)
self.config = config
self.has_mfa = False
self.mfa_id = None
self.token_header = None
self.AUTHENTICATION_METHODS = {
'resume': self.handle_polling,
}
self.__states__ = ('has_mfa', 'mfa_id', 'token_header')
def locate_browser(self, state):
if not self.token_header:
# Do a new login if no token_header
return
# Set token_header if the URL in the state needs it.
# This token must not be set for the full session,
# some URLs will crash if we provide them this header.
try:
super().locate_browser(state)
except ClientError as e:
if (
e.response.status_code == 401
and e.response.json()['message'] == 'WEB Authentication Required'
):
self.location(state['url'], headers=self.token_header)
else:
raise
def init_login(self):
if self.has_mfa:
# If mfa is enabled we will not be able to stop
# the login before the notification is sent.
self.check_interactive()
# Same uuid must be used for config_page and login page
# config_page does not return anything useful for us but we must do
# a GET request on it or we will have a 403 later on the login page
uuid = str(uuid4())
params = {
"site": "m1st",
"manufacturer": "Mozilla",
"model": "X11",
"platform": "web",
"uuid": uuid,
"version": "Linux Linux x86_64",
"navigateur": "firefox",
"navigateurVersion": "78.0.0",
}
self.config_page.go(params=params)
# Check if account is temporarily blocked
self.authenticate_fails.go(json={"username": self.username})
connexion_status = self.response.json()
if connexion_status == 3:
raise BrowserUserBanned('Votre compte a été temporairement bloqué pour des raisons de sécurité (3 tentatives successives erronées).')
# Hardcoded website_key because the HTML containing it is dynamically generated by JS
website_key = '6LdBGWYUAAAAAK-5wpJNH0u1RrtIBVZI2xh1mixt'
website_url = self.BASEURL
captcha_response = self.config['captcha_response'].get()
if not captcha_response:
raise RecaptchaV2Question(website_key=website_key, website_url=website_url)
data = {
'site': 'm1st',
'username': self.username,
'password': self.password,
'captcha': captcha_response,
'uuid': uuid,
'platform': 'web',
'country': '',
'city': '',
}
try:
self.login.go(json=data)
self.mfa_id = self.page.get_mfa_id()
token = self.page.get_token()
if self.mfa_id and not token:
self.has_mfa = True
raise AppValidation(
message="L’accès à votre espace personnel est en attente de la validation depuis votre téléphone mobile. "
+ "Veuillez cliquer sur la notification de votre téléphone mobile pour valider l’accès à votre espace personnel."
)
if 'epargnant' not in self.page.get_current_domain():
# Able only to handle subspaces behind domains 'epargnant.amundi or epargnant.cal-els'
# TODO: handle amundi-ee.com/account website
raise NotImplementedWebsite()
self.has_mfa = False
self.token_header = {'X-noee-authorization': token}
except ClientError as e:
if e.response.status_code == 401:
message = e.response.json().get('message', '')
# Wrong username
if 'problem on profile for' in message.lower():
raise BrowserIncorrectPassword(message)
# No other way to know if we have a wrong password
if e.response.status_code == 403:
raise BrowserIncorrectPassword()
raise
def handle_polling(self):
for _ in range(60):
try:
self.mfa_status.go(mfa_id=self.mfa_id)
except ClientError as error:
if error.response.status_code == 403: # no message
raise AppValidationCancelled()
raise AssertionError('Unhandled error during mfa')
else:
token = self.page.get_token()
if not token:
time.sleep(3)
continue
if 'epargnant' not in self.page.get_current_domain():
# Able only to handle subspaces behind domains 'epargnant.amundi or epargnant.cal-els'
# TODO: handle amundi-ee.com/account website
raise NotImplementedWebsite()
self.token_header = {'X-noee-authorization': token}
return
raise AppValidationExpired()
@staticmethod
def merge_accounts(accounts):
# merge accounts that have a master id to the master account
master_accounts_list = []
for account in accounts:
if account._is_master:
master_accounts_list.append(account)
if not master_accounts_list:
# There is no master_account.
yield from [account for account in accounts if account.balance > 0]
return
for account in accounts:
# If the account is not a PERCOL and has a positive balance,
# we fetch it and check the next one.
if account.type != Account.TYPE_PERCO and account.balance > 0:
yield account
continue
# The account is a PERCOL with a positive balance.
# Case 1: The account has no master and is not a master.
elif account.balance > 0 and not account._is_master and not account._master_id:
yield account
continue
for master_account in master_accounts_list:
# Case 2: The account has a master account but both of them got a positive balance.
# We need to remove "Piloté" or "Libre" from the master account label.
if all((
account._master_id == master_account._id_dispositif,
account.balance > 0,
master_account.balance > 0,
('Piloté' in account.label and 'Libre' in master_account.label)
or ('Piloté' in master_account.label and 'Libre' in account.label),
)):
master_account.label = re.sub(r' Piloté| Libre', '', master_account.label)
# Case 3: The account has a master.
if account.balance > 0 and account._master_id == master_account._id_dispositif:
master_account._sub_accounts.append(account)
for master_account in master_accounts_list:
# If the master account has a balance of 0, we need to assign the first sub_account id
# with a positive balance to replace the original master account id.
# Otherwise, the PSU would be unable to access the master account
# if this account was previously 'deactivated' due to a balance of 0.
if master_account.balance == 0:
for account in master_account._sub_accounts:
master_account.id = account.id
break
# We aggregate the master account with all his sub_accounts.
for account in master_account._sub_accounts:
master_account.balance += account.balance
if master_account.balance > 0:
yield master_account
@need_login
def iter_accounts(self):
self.accounts.go(headers=self.token_header)
company_name = self.page.get_company_name()
if empty(company_name):
self.logger.warning('Could not find the company name for these accounts.')
accounts = list(self.page.iter_accounts(username=self.username))
# We need to store the existing link between active PEEs and disabled ones.
# Empty PEEs (balance to 0) can have transactions that we need to retrieve.
# Since empty PEEs are ignored, if it's linked to an active PEE, we need to
# transfer his history in the active linked account.
for account in accounts:
# We only retrieve link of PEE accounts with null balances.
if all((
account._code_dispositif_lie,
account.balance == 0,
account.type == Account.TYPE_PEE,
)):
for acc in accounts:
if acc.balance > 0 and account._code_dispositif_lie == acc.id:
acc._linked_accounts.append(account.id)
for account in self.merge_accounts(accounts):
account.company_name = company_name
yield account
@need_login
def iter_investment(self, account):
if account.balance == 0:
self.logger.info('Account %s has a null balance, no investment available.', account.label)
return
self.accounts.stay_or_go(headers=self.token_header)
ignored_urls = (
'www.sggestion-ede.com/product', # Going there leads to a 404
'www.assetmanagement.hsbc.com', # Information not accessible
)
handled_urls = (
'www.amundi.fr/fr_part', # AmundiInvestmentsPage
'funds.amundi-ee.com/productsheet', # EEInvestmentDetailPage & EEInvestmentPerformancePage
'www.amundi-ee.com/part/home_fp', # EEInvestmentDetailPage & EEInvestmentPerformancePage
'www.amundi-ee.com/product', # EEProductInvestmentPage
'fr.allianzgi.com/fr-fr', # AllianzInvestmentPage
'www.eres-group.com/eres', # EresInvestmentPage
'www.cpr-am.fr/particuliers/product', # CprInvestmentPage
'www.epargne-retraite-entreprises.bnpparibas.com', # BNPInvestmentPage
'axa-im.fr/fonds', # AxaInvestmentPage
'www.epsens.com/information-financiere', # EpsensInvestmentPage
'www.ecofi.fr/fr/fonds/dynamis-solidaire', # EcofiInvestmentPage
'www.societegeneralegestion.fr', # SGGestionInvestmentPage
'https://ims.olisnet.com/extranet', # OlisnetInvestmentPage
'www.labanquepostale-am.fr/fr/nos-fonds', # BPESInvestmentDetailsPage
)
def aggregate_investments(investments):
# Aggregate investments with the same label within the same account.
aggregated_investments = dict()
for inv in investments:
existing_investment = aggregated_investments.get(inv.label)
if existing_investment:
existing_investment.valuation += inv.valuation
if not empty(existing_investment.quantity):
existing_investment.quantity += inv.quantity
if not empty(existing_investment.diff):
existing_investment.diff += inv.diff
else:
aggregated_investments[inv.label] = inv
yield inv
def iter_investment_from_account(account):
for inv in self.page.iter_investments(account_id=account.id, account_type=account.type):
if inv._details_url:
# Only go to known details pages to avoid logout on unhandled pages
if any(url in inv._details_url for url in handled_urls):
self.fill_investment_details(inv)
else:
if not any(url in inv._details_url for url in ignored_urls):
# Not need to raise warning if the URL is already known and ignored
self.logger.warning('Investment details on URL %s are not handled yet.', inv._details_url)
inv.asset_category = NotAvailable
inv.recommended_period = NotAvailable
yield inv
investments = []
account_ids = []
if account._is_master and account._sub_accounts:
for sub_account in account._sub_accounts:
account_ids.append(sub_account.id)
if sub_account.balance == 0:
self.logger.info('Account %s has a null balance, no investment available.', sub_account.label)
continue
investments.extend(list(iter_investment_from_account(sub_account)))
self.accounts.stay_or_go(headers=self.token_header)
if account.id not in account_ids:
investments.extend(list(iter_investment_from_account(account)))
return aggregate_investments(investments)
@need_login
def fill_investment_details(self, inv):
# Going to investment details may lead to various websites.
# This method handles all the already encountered pages.
try:
self.location(inv._details_url)
except (ServerError, BrowserHTTPNotFound):
# Some URLs return a 500 or a 404 even on the website
self.logger.warning('Details are not available for this investment.')
inv.asset_category = NotAvailable
inv.recommended_period = NotAvailable
return inv
# Pages with only asset category available
if self.allianz_investments.is_here():
inv.asset_category = self.page.get_asset_category()
inv.recommended_period = NotAvailable
# Pages with asset_category & perfomance
if self.axa_investments.is_here():
params = self.page.get_redirection_params()
fund_id = re.search(r'(\d+)', self.url.split('-')[-1]).group(1)
space = re.search(r'https:\/\/(\w+).axa', self.url).group(1)
self.axa_inv_api_redirection.go(space=space, fund_id=fund_id, params=params)
self.page.get_asset_category(obj=inv)
api_fund_id = self.page.get_api_fund_id()
self.axa_inv_api.go(space=space, api_fund_id=api_fund_id)
self.page.fill_investment(obj=inv)
# Pages with asset category & recommended period
elif any((
self.eres_investments.is_here(),
self.ee_product_investments.is_here(),
self.epsens_investments.is_here(),
self.ecofi_investments.is_here(),
)):
self.page.fill_investment(obj=inv)
# Particular cases
elif (
self.ee_investments.is_here()
or self.amundi_investments.is_here()
):
if self.ee_investments.is_here():
inv.recommended_period = self.page.get_recommended_period()
details_url = self.page.get_details_url()
performance_url = self.page.get_performance_url()
if details_url:
self.location(details_url)
if self.investment_details.is_here():
inv.recommended_period = inv.recommended_period or self.page.get_recommended_period()
inv.asset_category = self.page.get_asset_category()
if performance_url:
self.location(performance_url)
if self.performance_details.is_here():
# The investments JSON only contains 1 & 5 years performances
# If we can access EEInvestmentPerformancePage, we can fetch all three
# values (1, 3 and 5 years), in addition the values are more accurate here.
complete_performance_history = self.page.get_performance_history()
if complete_performance_history:
inv.performance_history = complete_performance_history
elif (
self.sg_gestion_investments.is_here()
or self.cpr_investments.is_here()
):
# Fetch asset category & recommended period
self.page.fill_investment(obj=inv)
# Fetch all performances on the details page
performance_url = self.page.get_performance_url()
if performance_url:
self.location(performance_url)
complete_performance_history = self.page.get_performance_history()
if complete_performance_history:
inv.performance_history = complete_performance_history
elif self.bnp_investments.is_here():
# We fetch the fund ID and get the attributes directly from the BNP-ERE API
fund_id = self.page.get_fund_id()
if fund_id:
# Specify the 'Accept' header otherwise the server returns WSDL instead of JSON
self.bnp_investment_api.go(fund_id=fund_id, headers={'Accept': 'application/json'})
self.page.fill_investment(obj=inv)
else:
self.logger.warning('Could not fetch the fund_id for BNP investment %s.', inv.label)
inv.asset_category = NotAvailable
inv.recommended_period = NotAvailable
elif self.olisnet_investments.is_here():
graph_id = self.page.get_graph_id()
self.olisnet_investments.go(action='benchmark.jsp')
inv.performance_history[5] = self.page.get_performance()
for year in (1, 3):
self.olisnet_investments.go(action='duree.jsp', params={'cs': graph_id, 'duree': f'{year}a'})
self.olisnet_investments.go(action='benchmark.jsp')
inv.performance_history[year] = self.page.get_performance()
return inv
@need_login
def iter_pockets(self, account):
if account.balance == 0:
self.logger.info('Account %s has a null balance, no pocket available.', account.label)
return
self.accounts.stay_or_go(headers=self.token_header)
def iter_pocket_from_account(account):
for investment in self.page.iter_investments(account_id=account.id, account_type=account.type):
for pocket in investment._pockets:
pocket.investment = investment
pocket.label = investment.label
yield pocket
if account._is_master and account._sub_accounts:
sub_accounts_id = []
for sub_account in account._sub_accounts:
sub_accounts_id.append(sub_account.id)
yield from iter_pocket_from_account(sub_account)
if account.id not in sub_accounts_id:
# Master account haven't a sub-account for itself
# We have to fetch pocket directly
yield from iter_pocket_from_account(account)
else:
yield from iter_pocket_from_account(account)
@need_login
def iter_history(self, account):
self.account_history.go(headers=self.token_header)
transactions = []
if account._is_master:
for sub_account in account._sub_accounts:
for tr in self.page.iter_history(account=sub_account):
if tr not in transactions:
transactions.append(tr)
for tr in self.page.iter_history(account=account):
if tr not in transactions:
transactions.append(tr)
return sorted_transactions(transactions)
class EEAmundi(AmundiBrowser):
# Careful if you modify the BASEURL, also verify Amundi's Children modules
BASEURL = 'https://epargnant.amundi-ee.com/'
class TCAmundi(AmundiBrowser):
# Careful if you modify the BASEURL, also verify Amundi's Children modules
BASEURL = 'https://epargnant.amundi-tc.com/'
class CAAmundi(AmundiBrowser):
# Careful if you modify the BASEURL, also verify Amundi's Children modules
BASEURL = 'https://epargnant.amundi-ca-assurances.com/'
class ESAmundi(AmundiBrowser):
# Careful if you modify the BASEURL, also verify Amundi's Children modules
BASEURL = 'https://www.amundi-ee.com/account/'
keyboard = URL(r'public/virtualKeyboard', LoginPage)
login = URL(r'public/authenticate', LoginPage)
accounts = URL(
r'api/individu/positionsFonds\?inclurePositionVide=false&flagUrlFicheFonds=true',
ESAccountsPage,
)
@retry(VirtKeyboardError, delay=0)
def get_vk_password(self):
""" Transform password to vk_password
Vk image is sent in base64.
For each keyboard the numbers are formatted in a unique style.
So if we download an unknown vk, VirtKeyboardError is raised
and we will retry with another vk.
If VirtKeyboardError happens too often, please add more
vk image in ESAmundiVirtKeyboard.
"""
self.keyboard.go()
keyboard = self.page.get_keyboard()
vk_password = self.page.create_vk_password(self.password, keyboard)
return keyboard, vk_password
def do_login(self):
captcha_response = self.config['captcha_response'].get()
if not captcha_response:
self.go_home()
self.config_page.go()
captcha_key = self.page.get_captcha_key()
raise RecaptchaV2Question(website_key=captcha_key, website_url=self.BASEURL)
keyboard, vk_password = self.get_vk_password()
json_data = {
'captcha': captcha_response,
'idKeyboard': keyboard['id'],
'password': vk_password,
'username': self.username,
}
try:
self.login.go(json=json_data)
except ClientError as err:
# Absolutely no way to know which json_data field is wrong
if err.response.status_code == 403:
raise BrowserIncorrectPassword()
raise
self.token_header = {'X-noee-authorization': self.page.get_token()}
es_virtkeyboard_page.py 0000664 0000000 0000000 00000017644 14575653726 0032743 0 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-amundi/modules/amundi # Copyright(C) 2023 Powens
#
# flake8: compatible
# 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 io import BytesIO
import base64
from woob.tools.captcha.virtkeyboard import SimpleVirtualKeyboard
class ESAmundiVirtKeyboard(SimpleVirtualKeyboard):
symbols = {
'0': (
'c52d792420f50aad8a67aa5a195a4b71',
'6d0ef26ea44efc0271b556c8714cdab8',
'd8759fed56704adeffa850f8820b7199',
'878f9ffc62af5b26d59494ae4d9fb74b',
'79080c81ba83923af7f17d26b8901fd8',
'8f032ed507eebb0583e00e408626ba0e',
'a39c2fbe668984b7ab3d62efea95a4ef',
'1a42437cbea82dda5dc19b2ef56c08b1',
'aababb52093904e1af0780fc9b482812',
'9b2c2831ae741442f7882dd661626fc7',
'a7a594b898f6953955d49fc856ad753f',
'bf9370633816bcb1acf0bb829d29c0a0',
'ef52e77262455c3c139ee022b12b6e6d',
),
'1': (
'f38053babe50d398ad637ed7bcfadd23',
'6f9b40f279a24b5ab72186588d86d9c0',
'3ef5f4abc51a8a27aa69fd3d5b4cebd5',
'721ce5c871745573a452f3aaa6a862af',
'2c36ba422057d36b897a27479c49892c',
'8d8bbc42953e2a128a5dfa9fc29f6eef',
'0c6b060f4a746a7d0a41a5763b1fbc7f',
'0559742827d7fd057e0a251ca5c5fb8c',
'596880933ad1419cf0ef86385c5dbdea',
'fd30f81fbf955a742c6475033e59d5d9',
'7637e06f897e51ed403ff0923a24249c',
'f1139ad6caa3c50f89e2c17f9b238f35',
'c1d18795f6e4722841464dacba115661',
),
'2': (
'd93b61347b890ef7df05cc5e1e8801bd',
'45cea12a492cc2a6cce090562ab3f663',
'531bf9713c9f37fb22c124867c1c2040',
'79811fe9311637d2a40e73e74bea4f4c',
'052ec4a212b6aa7323c4fef7738ecd72',
'f0cfdb209ad508b0d24cb582fbc01b05',
'5f82b6f838ae65c0c38944da391a758c',
'da248a943b292bd0526ea8cedda312a2',
'ad909091d65dc509ce93c2888a94b526',
'6e47cf930bf58476a6e32a4319542aa7',
'60aa66e9f326db99c764413c57381d9d',
'7c8ec36ffc68df8a8977f8ba158366f2',
'36b01df2c75d7bfe02565203ec55e707',
),
'3': (
'00d653c1e786b20080fa8f86be515113',
'a5e61e93d4ea9a8c3a604834f4e13468',
'339012a876a1ae6b88d1a06701ee2387',
'61daeb4d6717f25b0bf6efc05535ea10',
'322574489a1513d92f90aabbf940346e',
'6e3d448db17eec928c2889e4a75e69e3',
'38a5955df1dff109fae77b183d480962',
'fcf7d285c89bf6d1b687a1a27e00e67a',
'13685c22791512fbf5594b443a1b0eb7',
'3dc4b8dd59da0258aaae6af39b471ef3',
'2b9f6c9c3339112e94d02eaefeccc5b6',
'0343153e59ed97db11e756fb0004f680',
'c4ab760abd7a89d844093176a12d4893',
),
'4': (
'2f5b60b51779cb1b0f548be9ec134b42',
'bd3eed76e45caacb063f281ca954413b',
'b7233e7f26706f91c76e96df858b6f7f',
'0fd26faf69c0e9811290dfa0ca0c827f',
'c3f9eec5358062ec161a337f77856ce6',
'7bc849f754e0095d0c3660ad6421983d',
'4c90ed8be81736addcbde2f2613ecc5b',
'3d1255f22156d7bb941f228cf8b0f6c4',
'6f99aab35bebefba21727c7a820b5f58',
'b0217da514d105bb29b3d84aed2b76fd',
'0ca6f762c631e271f8bc5141fabfa34a',
'7a492c80d5db526523e4c75f07f9d59d',
'f3037ad69b9390dc870205e106219669',
),
'5': (
'10ed51da2fde546772ed97c942871e42',
'b8c3c70e4975d72e10ea57fa1bfb8523',
'c7a797ad2ddeba49e9e2572a1fdf6b99',
'08ecea5cd75433b7f22e3af75652d21c',
'a48893a66a761a2bb339f810d07636e6',
'891c80dd791ce4414272c15c6d130ebf',
'750de6a95d73d390449fe0d8a49f0378',
'52cfc01b24d5fc083aea487973f3d773',
'5ac68220c737d5331440e28792e62e84',
'2267f4b21e3e43bc68bf913b58023376',
'9eb76988dde7e6963c4c0e46555e2759',
'ccffea6a07584f48c084af8ea578d646',
'a6a32dd5ae0977703cf0ac15afc190bc',
),
'6': (
'15c116193e708c732bacaa8df8bc7de7',
'a5e61e93d4ea9a8c3a604834f4e13468',
'3a901bff6d04d5ec56ff5daca6e5447e',
'858a6aaf1bdd884c52444fe2bd001202',
'6c2bd071609fec1a3e47420df45c74b0',
'8e52882bcc105addf0cbdfb022439d47',
'b4b1ff0a0c8a5b48b2843bb0b85d46ae',
'895c85e96c60282c0e062042022292a9',
'0c7aeb4da9a63c463522bd044c5e03f4',
'068869a8da8d3350b5aba5fc7974eccb',
'df6dbd3c998eb8f0934af13deb5f816a',
'8a60a0ea1d3ca90bb0a23e4d2214041b',
'c07a9ca02dd249a05b452739e7cf3f69',
),
'7': (
'd0a670e31f27817d538501a85f2d49ec',
'5fe3218925d7eb2ca7af20363c2844cd',
'504e5afff16d7e1f0451579f1dd9d9d5',
'80410c1bf7b1689089538dfb0db1b666',
'3b1c2c70cf8959b223c194c796b256bb',
'c055a5104b82e0d88fa0bb2ebb8b7bec',
'dabb2df5f9307c304496254b7374eb22',
'00ee47abcc93f68fe52a70deb88da46e',
'726ded975ed84d6ef837cbb5648f97d5',
'c3754d8774a1186d048b241e99943bbc',
'1d837b17592e8cf782ed7de2b02016b1',
'f718f833478045809fd29d73dfb302cf',
'df763ebe92968c17eaa5b97a84aba257',
),
'8': (
'5e6930c12ad947550c319a41df066cd9',
'bd3eed76e45caacb063f281ca954413b',
'af7852b52a8f3fa8a659382fd03f0c1d',
'fbe20c7f35b68205e674c6e46a2035b3',
'668cb0da6e65af530f5f90f6296425ce',
'f2ecb185a0e75d908dd16bc3caceea76',
'29453a8672edab5cff5b35d83e242857',
'd5a9c20820e4f2d70cb75f578cdd8ad1',
'1b23565c71769383a62c7d161d4f20f5',
'db20519e941c7f79915c2c9113739f10',
'f311bef7e324ae09c14362768b21ce32',
'28dd98545832c587fd4d25c194b362a5',
'e51d754619d1d70c0a7a3b8a8a704ffa',
),
'9': (
'23756eceb8350d46b2cb1e1d7b6ae3ec',
'c73a225b1e9731e5b2eb903d493bae5e',
'c61bd57f3573fecda4de63ee3563ce4a',
'df27eef7806c02e5ebc25d39a0c59170',
'08ed18417136dc88723a5b95e2377fcc',
'f74586a7b4a20732de85a68b809c40f4',
'ee202b236276d06fe8bed7301bf571a4',
'ed0136c48f7370476e5769bdea6bc345',
'04141d46eccd35efa44ea022171ce92e',
'bd0bb1634c4696ac262a7c7f57e33e2f',
'62cca6f12a368e2e5a4d8495e3089065',
'80c426ff8ffa1282c6da5795932da62b',
'e1e9ef14c6cc46c9bc4217203f547441',
),
}
matching_symbols_coords = {
'0': (0, 0, 65, 50),
'1': (70, 0, 135, 50),
'2': (140, 0, 205, 50),
'3': (210, 0, 275, 50),
'4': (280, 0, 345, 50),
'5': (0, 55, 65, 105),
'6': (70, 55, 135, 105),
'7': (140, 55, 205, 105),
'8': (210, 55, 275, 105),
'9': (280, 55, 345, 105),
}
def __init__(self, browser, image_base64):
image_file = BytesIO(base64.b64decode(image_base64))
super(ESAmundiVirtKeyboard, self).__init__(
image_file,
cols=5,
rows=2,
matching_symbols_coords=self.matching_symbols_coords,
)
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-amundi/modules/amundi/favicon.png 0000664 0000000 0000000 00000021603 14575653726 0030401 0 ustar 00root root 0000000 0000000 PNG
IHDR @ @ zTXtRaw profile type exif xŜk8cg XgwKPRmk%5[zde16ßdB%Ք.k|(W{zǯq?V88g>.b`/Z{ǀ|1o+_qc 3zL}́ஜSsqWsj>_yϯ:䖷yw7ly_)0pz66ɟG0xy5x}=@ɭ6~9_qfT>gսg{/Bb鱨R'esZ/9Ep\xzkQux`mvuއL12
c_T7s]O_Ax\n=Mg**/[1/f^.aS