pax_global_header 0000666 0000000 0000000 00000000064 13514067645 0014525 g ustar 00root root 0000000 0000000 52 comment=6d52c0c92f2617476fe0b131c1eebba68676b2fb
woob-6d52c0c92f2617476fe0b131c1eebba68676b2fb-modules-n26/ 0000775 0000000 0000000 00000000000 13514067645 0021620 5 ustar 00root root 0000000 0000000 woob-6d52c0c92f2617476fe0b131c1eebba68676b2fb-modules-n26/modules/ 0000775 0000000 0000000 00000000000 13514067645 0023270 5 ustar 00root root 0000000 0000000 woob-6d52c0c92f2617476fe0b131c1eebba68676b2fb-modules-n26/modules/n26/ 0000775 0000000 0000000 00000000000 13514067645 0023675 5 ustar 00root root 0000000 0000000 woob-6d52c0c92f2617476fe0b131c1eebba68676b2fb-modules-n26/modules/n26/__init__.py 0000664 0000000 0000000 00000001522 13514067645 0026006 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Benjamin Bouvier
#
# This file is part of a weboob module.
#
# This weboob 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 weboob 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 weboob module. If not, see .
from .module import Number26Module
__all__ = ['Number26Module']
woob-6d52c0c92f2617476fe0b131c1eebba68676b2fb-modules-n26/modules/n26/browser.py 0000664 0000000 0000000 00000013737 13514067645 0025745 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Benjamin Bouvier
#
# This file is part of a weboob module.
#
# This weboob 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 weboob 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 weboob module. If not, see .
from decimal import Decimal
from datetime import datetime
from functools import wraps
from weboob.browser.browsers import DomainBrowser
from weboob.capabilities.base import find_object, NotAvailable
from weboob.capabilities.bank import Account, Transaction, AccountNotFound
from weboob.browser.filters.standard import CleanText
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.browser.exceptions import ClientError
# Do not use an APIBrowser since APIBrowser sends all its requests bodies as
# JSON, although N26 only accepts urlencoded format.
def need_login(func):
@wraps(func)
def wrapper(browser, *args, **kwargs):
if browser.auth_method.lower() == 'basic':
browser.do_login()
return func(browser, *args, **kwargs)
return wrapper
class Number26Browser(DomainBrowser):
BASEURL = 'https://api.tech26.de'
# Password encoded in base64 for the initial basic-auth scheme used to
# get an access token.
INITIAL_TOKEN = 'bXktdHJ1c3RlZC13ZHBDbGllbnQ6c2VjcmV0'
def request(self, *args, **kwargs):
"""
Makes it more convenient to add the bearer token and convert the result
body back to JSON.
"""
kwargs.setdefault('headers', {})['Authorization'] = self.auth_method + ' ' + self.bearer
kwargs.setdefault('headers', {})['Accept'] = "application/json"
return self.open(*args, **kwargs).json()
def __init__(self, username, password, *args, **kwargs):
super(Number26Browser, self).__init__(*args, **kwargs)
self.username = username
self.password = password
self.auth_method = 'Basic'
self.bearer = Number26Browser.INITIAL_TOKEN
def do_login(self):
data = {
'username': self.username,
'password': self.password,
'grant_type': 'password'
}
try:
result = self.request('/oauth/token', data=data, method="POST")
except ClientError as ex:
response = ex.response.json()
if response.get('error') == 'invalid_grant':
raise BrowserIncorrectPassword(response['error_description'])
if response.get('title') == 'Error':
raise BrowserUnavailable(response['message'])
raise
self.auth_method = 'bearer'
self.bearer = result['access_token']
@need_login
def get_accounts(self):
account = self.request('/api/accounts')
spaces = self.request('/api/spaces')
a = Account()
# Number26 only provides a checking account (as of sept 19th 2016).
a.type = Account.TYPE_CHECKING
a.label = u'Checking account'
a.id = account["id"]
a.number = NotAvailable
a.balance = Decimal(str(spaces["totalBalance"]))
a.iban = account["iban"]
a.currency = u'EUR'
return [a]
def get_account(self, _id):
return find_object(self.get_accounts(), id=_id, error=AccountNotFound)
@need_login
def get_categories(self):
"""
Generates a map of categoryId -> categoryName, for fast lookup when
fetching transactions.
"""
categories = self.request('/api/smrt/categories')
cmap = {}
for c in categories:
cmap[c["id"]] = c["name"]
return cmap
@staticmethod
def is_past_transaction(t):
return "userAccepted" in t or "confirmed" in t
@need_login
def get_transactions(self, categories):
return self._internal_get_transactions(categories, Number26Browser.is_past_transaction)
@need_login
def get_coming(self, categories):
filter = lambda x: not Number26Browser.is_past_transaction(x)
return self._internal_get_transactions(categories, filter)
@need_login
def _internal_get_transactions(self, categories, filter_func):
TYPES = {
'PT': Transaction.TYPE_CARD,
'AA': Transaction.TYPE_CARD,
'CT': Transaction.TYPE_TRANSFER,
'WEE': Transaction.TYPE_BANK,
}
transactions = self.request('/api/smrt/transactions?limit=1000')
for t in transactions:
if not filter_func(t) or t["amount"] == 0:
continue
new = Transaction()
new.date = datetime.fromtimestamp(t["createdTS"] / 1000)
new.rdate = datetime.fromtimestamp(t["visibleTS"] / 1000)
new.id = t['id']
new.amount = Decimal(str(t["amount"]))
if "merchantName" in t:
new.raw = new.label = t["merchantName"]
elif "partnerName" in t:
new.raw = CleanText().filter(t["referenceText"]) if "referenceText" in t else CleanText().filter(t["partnerName"])
new.label = t["partnerName"]
else:
new.raw = new.label = ''
if "originalCurrency" in t:
new.original_currency = t["originalCurrency"]
if "originalAmount" in t:
new.original_amount = Decimal(str(t["originalAmount"]))
new.type = TYPES.get(t["type"], Transaction.TYPE_UNKNOWN)
if t["category"] in categories:
new.category = categories[t["category"]]
yield new
woob-6d52c0c92f2617476fe0b131c1eebba68676b2fb-modules-n26/modules/n26/favicon.png 0000664 0000000 0000000 00000000663 13514067645 0026035 0 ustar 00root root 0000000 0000000 PNG
IHDR @ @ iq bKGD pHYs tIME iTXtComment Created with GIMPd.e IDATx1jQECBll!+VE
;{{w2"=$i&03o{=|\~xx ""rLw5S̃3TuFMdL+X=eָl'`90RQw W`s`Tbn.l!9|ŒXO0ni@!7 j
H6$bbh""ڠ6
jڠ6
jڠ6("Eء(n):qSMQPDD[~ IENDB` woob-6d52c0c92f2617476fe0b131c1eebba68676b2fb-modules-n26/modules/n26/module.py 0000664 0000000 0000000 00000004316 13514067645 0025540 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Benjamin Bouvier
#
# This file is part of a weboob module.
#
# This weboob 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 weboob 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 weboob module. If not, see .
from weboob.capabilities.bank import CapBank
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import Value, ValueBackendPassword
from .browser import Number26Browser
__all__ = ['Number26Module']
class Number26Module(Module, CapBank):
NAME = 'n26'
DESCRIPTION = u'Bank N26'
MAINTAINER = u'Benjamin Bouvier'
EMAIL = 'public@benj.me'
LICENSE = 'LGPLv3+'
VERSION = '1.6'
BROWSER = Number26Browser
CONFIG = BackendConfig(
Value('login', label='Email', regexp='.+'),
ValueBackendPassword('password', label='Password')
)
STORAGE = {'categories': {}}
def get_categories(self):
categories = self.storage.get("categories", None)
if categories is None:
categories = self.browser.get_categories()
self.storage.set("categories", categories)
return categories
def create_default_browser(self):
return self.create_browser(self.config['login'].get(), self.config['password'].get())
def iter_accounts(self):
return self.browser.get_accounts()
def get_account(self, id):
return self.browser.get_account(id)
def iter_history(self, account):
categories = self.get_categories()
return self.browser.get_transactions(categories)
def iter_coming(self, account):
categories = self.get_categories()
return self.browser.get_coming(categories)