pax_global_header 0000666 0000000 0000000 00000000064 14575653726 0014536 g ustar 00root root 0000000 0000000 52 comment=5f3d558793b537a74480241ac6981479f5938cd3
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-swile/ 0000775 0000000 0000000 00000000000 14575653726 0023165 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-swile/modules/ 0000775 0000000 0000000 00000000000 14575653726 0024635 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-swile/modules/swile/ 0000775 0000000 0000000 00000000000 14575653726 0025760 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-swile/modules/swile/__init__.py 0000664 0000000 0000000 00000001426 14575653726 0030074 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2018 Roger Philibert
#
# This file is part of woob.
#
# woob 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.
#
# woob 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 woob. If not, see .
from .module import SwileModule
__all__ = ['SwileModule']
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-swile/modules/swile/browser.py 0000664 0000000 0000000 00000021763 14575653726 0030026 0 ustar 00root root 0000000 0000000 # Copyright(C) 2018 Roger Philibert
#
# This file is part of woob.
#
# woob 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.
#
# woob 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 woob. If not, see .
# flake8: compatible
from datetime import date, timedelta
from functools import wraps
from woob.browser.filters.standard import (
CleanDecimal, CleanText, DateTime, Currency,
Format,
)
from woob.capabilities.base import empty
from woob.capabilities.captcha import RecaptchaV3Question
from woob.browser.filters.json import Dict
from woob.browser.exceptions import ClientError, BrowserTooManyRequests
from woob.exceptions import BrowserIncorrectPassword, WrongCaptchaResponse, BrowserUserBanned
from woob.browser.browsers import APIBrowser, OAuth2Mixin
from woob.capabilities.bank import Account, Transaction
def need_login(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if not self.logged:
self.do_login()
return func(self, *args, **kwargs)
return wrapper
class SwileBrowser(OAuth2Mixin, APIBrowser):
BASEURL = 'https://customer-api.swile.co'
ACCESS_TOKEN_URI = 'https://directory.swile.co/oauth/token'
client_id = '533bf5c8dbd05ef18fd01e2bbbab3d7f69e3511dd08402862b5de63b9a238923'
def __init__(self, config, *args, **kwargs):
super(SwileBrowser, self).__init__(*args, **kwargs)
self.session.headers['X-API-Key'] = '50558e8b836b7a8e089c35b7b58a1d3959ca56d6'
self.session.headers['X-Lunchr-Platform'] = 'web'
self.session.headers['X-Lunchr-App-Version'] = '0.1.0'
self.credentials = {
'client_id': self.client_id,
'grant_type': "password",
'username': config['login'].get(),
'password': config['password'].get(),
}
self.config = config
def request_authorization(self):
try:
if self.config['captcha_response'].get():
self.credentials['recaptcha_enterprise_token'] = self.config['captcha_response'].get()
self.location(self.ACCESS_TOKEN_URI, data=self.credentials)
except ClientError as e:
if e.response.status_code == 401:
if not self.config['captcha_response'].get():
raise RecaptchaV3Question(
website_url='https://app.swile.co/signin',
website_key='6LfrQZEdAAAAAJZF_2WDGlcqQ_6hNIN55Mi7Eiyn',
is_enterprise=True,
action='login'
)
elif not e.response.json():
# Captcha is wrong when empty json is returned
raise WrongCaptchaResponse()
json = e.response.json()
message = json.get('error_description', '')
if e.response.status_code == 400:
if 'authorization grant is invalid, expired, revoked' in message:
# JS interprets this message as wrongpass
raise BrowserIncorrectPassword()
# sometimes we get a 401 error with an invalid token when we try to connect several times
# with wrong creds to our swile account
if e.response.status_code == 401:
error = json.get('error', '')
error_code = json.get('error_code', '')
if error == 'invalid_token':
if 'try again later' in message.lower():
raise BrowserUserBanned(message)
# we can also get a wrongcaptcha error due to these wrong creds (conclusion of several tests)
elif 'recaptcha_invalid_token' in error_code.lower():
raise WrongCaptchaResponse(error_code)
if e.response.status_code == 429:
raise BrowserTooManyRequests()
raise AssertionError(f'Unhandled error message during login: {message}')
self.update_token(self.response.json())
@need_login
def get_me(self):
return self.request(self.absurl('/api/v0/users/me', base=self.BASEURL))['user']
@need_login
def get_account(self):
json = self.get_me()
account = Account()
account.id = CleanText(Dict('id'))(json)
account.number = account.id
account.bank_name = 'Swile'
account.type = Account.TYPE_CHECKING
# Check if account have a card
balance = Dict('meal_voucher_info/balance/value', default=None)(json)
if empty(balance):
return
account.balance = CleanDecimal.SI(balance)(json)
account.label = Format('%s %s', CleanText(Dict('first_name')), CleanText(Dict('last_name')))(json)
account.currency = Currency(Dict('meal_voucher_info/balance/currency/iso_3'))(json)
account.cardlimit = CleanDecimal.SI(Dict('meal_voucher_info/daily_balance/value'))(json)
yield account
@need_login
def iter_history(self, account):
# make sure we have today's transactions
before = date.today() + timedelta(days=1)
for _ in range(200): # limit pagination
response = self.open(
'https://banking-api.swile.co/api/v0/payments_history',
params={
'per': 20,
'before': before.isoformat(),
# don't pass page= param, it works but
# it's slower than the before= param
},
)
json = response.json()
if len(Dict('payments_history')(json)) == 0:
break
has_transactions = False
for payment in Dict('payments_history')(json):
if 'refunding_transaction' in payment:
refund = self._parse_transaction(payment['refunding_transaction'])
refund.type = Transaction.TYPE_CARD
yield refund
has_transactions = True
transaction = self._parse_transaction(payment)
if transaction:
# this is a millisecond-precise datetime (with a timezone).
# fortunately, the api excludes transactions occuring at the exact datetime we pass.
# if the page boundary is hit on transactions occurring at the same datetime, we might lose some of them though.
before = transaction.date
yield transaction
has_transactions = True
if not has_transactions:
break
else:
raise Exception("that's a lot of transactions, probable infinite loop?")
def _parse_transaction(self, payment):
# Different types of payment
# ORDER = order on swile website
# LUNCHR_CARD_PAYMENT = pay in shop
# PAYMENT = pay with swile card or/and linked bank card
# MEAL_VOUCHER_CREDIT = refund
transaction = Transaction()
transaction_id = Dict('transaction_number', default=None)(payment)
# Check if transaction_id is None or declined date exists which indicates failed transaction
if transaction_id is None or Dict('declined_at', default=None)(payment):
return
# Check if transaction is only on cb card
# if 'details' is empty we put default on '' because it's probably a
# 'MEAL_VOUCHER_RENEWAL' or a 'MEAL_VOUCHER_EXPIRATION'
if (
Dict('type')(payment) != 'MEAL_VOUCHER_CREDIT'
and len(Dict('details', default='')(payment)) == 1
and Dict('details/0/type')(payment) == 'CREDIT_CARD'
):
return
# special case, if the payment is made from the platform with a card not linked to the swile card
if Dict('type')(payment) == 'ORDER' and not Dict('details')(payment):
return
transaction.id = transaction_id
transaction.date = DateTime(Dict('executed_at'))(payment)
transaction.rdate = DateTime(Dict('created_at'))(payment)
transaction.label = Dict('name')(payment)
if Dict('type')(payment) == 'MEAL_VOUCHER_CREDIT':
transaction.amount = CleanDecimal.US(Dict('amount/value'))(payment)
transaction.type = Transaction.TYPE_DEPOSIT
elif Dict('type')(payment) in ('MEAL_VOUCHER_RENEWAL', 'MEAL_VOUCHER_EXPIRATION'):
transaction.amount = CleanDecimal.US(Dict('amount/value'))(payment)
transaction.type = Transaction.TYPE_BANK
else:
transaction.amount = CleanDecimal.US(Dict('details/0/amount'))(payment)
transaction.type = Transaction.TYPE_CARD
return transaction
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-swile/modules/swile/module.py 0000664 0000000 0000000 00000003232 14575653726 0027617 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2018 Roger Philibert
#
# This file is part of woob.
#
# woob 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.
#
# woob 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 woob. If not, see .
# flake8: compatible
from woob.tools.backend import Module, BackendConfig
from woob.tools.value import ValueBackendPassword, ValueTransient
from woob.capabilities.bank import CapBank
from .browser import SwileBrowser
__all__ = ['SwileModule']
class SwileModule(Module, CapBank):
NAME = 'swile'
DESCRIPTION = 'Swile'
MAINTAINER = 'Roger Philibert'
EMAIL = 'roger.philibert@gmail.com'
LICENSE = 'LGPLv3+'
VERSION = '3.6'
BROWSER = SwileBrowser
CONFIG = BackendConfig(
ValueBackendPassword('login', label='E-mail', masked=False),
ValueBackendPassword('password', label='Mot de passe'),
ValueTransient('captcha_response', label='Captcha Response'),
)
def create_default_browser(self):
return self.create_browser(self.config)
def iter_accounts(self):
return self.browser.get_account()
def iter_history(self, account):
return self.browser.iter_history(account)
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-swile/modules/swile/requirements.txt 0000664 0000000 0000000 00000000014 14575653726 0031237 0 ustar 00root root 0000000 0000000 woob ~= 3.5