...
 
Commits (7)
# -*- coding: utf-8 -*-
# Copyright(C) 2018 Roger Philibert
#
# 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 __future__ import unicode_literals
from .module import LunchrModule
__all__ = ['LunchrModule']
# -*- coding: utf-8 -*-
# Copyright(C) 2018 Roger Philibert
#
# 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 __future__ import unicode_literals
from weboob.browser.filters.standard import CleanDecimal, CleanText, DateTime
from weboob.browser.filters.json import Dict
from weboob.browser.exceptions import ClientError
from weboob.exceptions import BrowserIncorrectPassword
from weboob.browser.browsers import APIBrowser
from weboob.capabilities.bank import Account, Transaction
class LunchrBrowser(APIBrowser):
BASEURL = 'https://api.lunchr.fr'
def __init__(self, login, password, *args, **kwargs):
"""LunchrBrowser needs login and password to fetch Lunchr API"""
super(LunchrBrowser, self).__init__(*args, **kwargs)
# self.session.headers are the HTTP headers for Lunchr API requests
self.session.headers['x-api-key'] = '644a4ef497286a229aaf8205c2dc12a9086310a8'
self.session.headers['x-lunchr-app-version'] = 'b6c6ca66c79ca059222779fe8f1ac98c8485b9f0'
self.session.headers['x-lunchr-platform'] = 'web'
# self.credentials is the HTTP POST data used in self._auth()
self.credentials = {
'user': {
'email': login,
'password': password,
}
}
def _auth(self):
"""Authenticate to Lunchr API using self.credentials.
If authentication succeeds, authorization header is set in self.headers
and response's json payload is returned unwrapped into dictionary.
"""
try:
response = self.open('/api/v0/users/login', data=self.credentials)
except ClientError as e:
json = e.response.json()
if e.response.status_code == 401:
message = json['result']['error']['message']
raise BrowserIncorrectPassword(message)
raise e
json = Dict('user')(response.json())
self.session.headers['Authorization'] = 'Bearer ' + Dict('token')(json)
return json
def get_account(self, id=None):
json = self._auth()
account = Account(id=Dict('id')(json))
# weboob.capabilities.bank.BaseAccount
account.bank_name = 'Lunchr'
account.label = CleanText(Dict('first_name'))(json) + ' ' + CleanText(Dict('last_name'))(json)
account.currency = CleanText(Dict('meal_voucher_info/balance/currency/iso_3'))(json)
# weboob.capabilities.bank.Account
account.type = Account.TYPE_CHECKING
account.balance = CleanDecimal(Dict('meal_voucher_info/balance/value'))(json)
account.cardlimit = CleanDecimal(Dict('meal_voucher_info/daily_balance/value'))(json)
return account
def iter_accounts(self):
yield self.get_account()
def iter_history(self, account):
page = 0
while True:
response = self.open('/api/v0/payments_history?page={:d}&per=20'.format(page))
json = response.json()
if len(Dict('payments_history')(json)) == 0:
break
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
transaction = self._parse_transaction(payment)
if transaction:
yield transaction
page += 1
if page >= Dict('pagination/pages_count')(json):
break
def _parse_transaction(self, payment):
transaction = Transaction()
transaction_id = Dict('transaction_number', default=None)(payment)
# Check if transaction_id is None which indicates failed transaction
if transaction_id is None:
return
transaction.id = transaction_id
transaction.date = DateTime(Dict('executed_at'))(payment)
transaction.rdate = DateTime(Dict('created_at'))(payment)
types = {
'ORDER': Transaction.TYPE_CARD, # order on lunchr website
'LUNCHR_CARD_PAYMENT': Transaction.TYPE_CARD, # pay in shop
'MEAL_VOUCHER_CREDIT': Transaction.TYPE_DEPOSIT,
# type can be null for refunds
}
transaction.type = types.get(Dict('type')(payment), Transaction.TYPE_UNKNOWN)
transaction.label = Dict('name')(payment)
transaction.amount = CleanDecimal(Dict('amount/value'))(payment)
return transaction
# -*- coding: utf-8 -*-
# Copyright(C) 2018 Roger Philibert
#
# 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 __future__ import unicode_literals
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword
from weboob.capabilities.base import find_object
from weboob.capabilities.bank import CapBank, AccountNotFound
from .browser import LunchrBrowser
__all__ = ['LunchrModule']
class LunchrModule(Module, CapBank):
NAME = 'lunchr'
DESCRIPTION = 'Lunchr'
MAINTAINER = 'Roger Philibert'
EMAIL = 'roger.philibert@gmail.com'
LICENSE = 'LGPLv3+'
VERSION = '1.6'
BROWSER = LunchrBrowser
CONFIG = BackendConfig(
ValueBackendPassword('login', label='E-mail', masked=False),
ValueBackendPassword('password', label='Mot de passe'),
)
def create_default_browser(self):
return self.create_browser(
self.config['login'].get(),
self.config['password'].get(),
)
def get_account(self, id):
return find_object(self.iter_accounts(), id=id, error=AccountNotFound)
def iter_accounts(self):
return self.browser.iter_accounts()
def iter_history(self, account):
return self.browser.iter_history(account)
......@@ -151,7 +151,7 @@ class Page(object):
self.params = params
# Setup encoding and build document
self.forced_encoding = encoding or self.ENCODING
self.forced_encoding = self.normalize_encoding(encoding or self.ENCODING)
if self.forced_encoding:
self.response.encoding = self.forced_encoding
self.doc = self.build_doc(self.data)
......@@ -169,7 +169,7 @@ class Page(object):
@property
def encoding(self):
return self.response.encoding
return self.normalize_encoding(self.response.encoding)
@encoding.setter
def encoding(self, value):
......@@ -223,6 +223,14 @@ class Page(object):
"""
return None
def normalize_encoding(self, encoding):
"""
Make sure we can easily compare encodings by formatting them the same way.
"""
if isinstance(encoding, bytes):
encoding = encoding.decode('utf-8')
return encoding.lower() if encoding else encoding
def absurl(self, url):
"""
Get an absolute URL from an a partial URL, relative to the Page URL
......@@ -518,7 +526,7 @@ class XMLPage(Page):
import re
m = re.search(b'<\?xml version="1.0" encoding="(.*)"\?>', self.data)
if m:
return m.group(1)
return self.normalize_encoding(m.group(1))
def build_doc(self, content):
import lxml.etree as etree
......@@ -664,10 +672,10 @@ class HTMLPage(Page):
Method to build the lxml document from response and given encoding.
"""
encoding = self.encoding
if encoding == 'latin-1':
encoding = 'latin1'
if encoding == u'latin-1':
encoding = u'latin1'
if encoding:
encoding = encoding.replace('ISO8859_', 'ISO8859-')
encoding = encoding.replace(u'iso8859_', u'iso8859-')
import lxml.html as html
parser = html.HTMLParser(encoding=encoding)
return html.parse(BytesIO(content), parser)
......@@ -681,18 +689,18 @@ class HTMLPage(Page):
# meta http-equiv=content-type content=...
_, params = parse_header(content)
if 'charset' in params:
encoding = params['charset'].strip("'\"")
encoding = self.normalize_encoding(params['charset'].strip("'\""))
for charset in self.doc.xpath('//head/meta[@charset]/@charset'):
# meta charset=...
encoding = charset.lower()
encoding = self.normalize_encoding(charset)
if encoding == 'iso-8859-1' or not encoding:
encoding = 'windows-1252'
if encoding == u'iso-8859-1' or not encoding:
encoding = u'windows-1252'
try:
codecs.lookup(encoding)
except LookupError:
encoding = 'windows-1252'
encoding = u'windows-1252'
return encoding
......