Skip to content

Commit

Permalink
adding nordigen importer
Browse files Browse the repository at this point in the history
  • Loading branch information
tarioch committed Sep 9, 2021
1 parent ea846a4 commit 3a3e5d2
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 63 deletions.
101 changes: 38 additions & 63 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,44 +29,8 @@ Dynamically generates prices to the base ccy by applying the fx rate to the base

price fetchers
--------------
**alphavantage**

Fetches prices from `Alphavantage <https://www.alphavantage.co/>`_
Requires the environment variable ``ALPHAVANTAGE_API_KEY`` to be set with your personal api key.

::

2019-01-01 commodity VWRL
price: "CHF:tariochbctools.plugins.prices.alphavantage/VWRL.SW"

**alphavantagefx**

Fetches fx rates from `Alphavantage <https://www.alphavantage.co/>`_
Requires the environment variable ``ALPHAVANTAGE_API_KEY`` to be set with your personal api key.

::

2019-01-01 commodity BTC
price: "CHF:tariochbctools.plugins.prices.alphavantagefx/BTC"


**bitstamp**

Fetches prices from `Bitstamp <https://www.bitstamp.com/>`_

::

2019-01-01 commodity BTC
price: "EUR:tariochbctools.plugins.prices.bitstamp/BTC"

**exchangeratesapi**

Fetches prices from `ratesapi.io <https://ratesapi.io>`_

::

2019-01-01 commodity EUR
price: "CHF:tariochbctools.plugins.prices.exchangeratesapi/EUR"
Also see `Beanprice <https://github.com/beancount/beanprice>`_

**interactivebrokers**

Expand All @@ -80,16 +44,6 @@ with a flex query that contains the open positions.
2019-01-01 commodity VWRL
price: "CHF:tariochbctools.plugins.prices.ibkr/VWRL"

**coinmarketcap**

Fetches prices from `coinmarketcap <https://coinmarketcap.com/>`_
Requires the environment variable ``COINMARKETCAP_API_KEY`` to be set to your api key.

::

2019-01-01 commodity BTC
price: "CHF:tariochbctools.plugins.prices.coinmarketcap/BTC"


importers
---------
Expand Down Expand Up @@ -161,6 +115,37 @@ Create a file called truelayer.yaml in your import location (e.g. download folde
client_secret: <CLIENT SECRET>
refresh_token: <REFRESH TOKEN>

**Nordigen**

Import from `Nordigen <http://nordigen.com/>`_ using their api services. e.g. supports Revolut.
You need to create a free account and create a token. I've included a small cli to allow to hook up
to different banks with nordigen. If you're country is not supported you can play around with other countries
e.g. CH is not allowed but things like revolut still work. You can also create multiple links and they will
all be listed in the end.

::

nordigen-conf list_banks --token YOURTOKEN --country DE
nordigen-conf create_link --token YOURTOKEN --bank REVOLUT_REVOGB21
nordigen-conf list_accounts --token YOURTOKEN list_accounts


::

from tariochbctools.importers.nordigen import importer as nordimp
CONFIG = [nordimp.Importer()]

Create a file called nordigen.yaml in your import location (e.g. download folder).

::

token: <TOKEN>

accounts:
- id: <ACCOUNT-ID>
asset_account: "Assets:MyAccount:CHF"


**zkb**

Import mt940 from `Zürcher Kantonalbank <https://www.zkb.ch/>`_
Expand All @@ -184,7 +169,12 @@ Create a file called ibkr.yaml in your import location (e.g. downloads folder).

**zak**

**Currently not working reliably**. Import PDF from `Bank Cler ZAK <https://www.cler.ch/de/info/zak/>`_
Import PDF from `Bank Cler ZAK <https://www.cler.ch/de/info/zak/>`_

::

from tariochbctools.importers.zak import importer as zakimp
CONFIG = [ zakimp.Importer(r'Kontoauszug.*\.pdf', 'Assets:ZAK:CHF') ]

**mt940**

Expand Down Expand Up @@ -283,18 +273,3 @@ Import CSV from `Neon <https://www.neon-free.ch/>`_

from tariochbctools.importers.neon import importer as neonimp
CONFIG = [neonimp.Importer('\d\d\d\d_account_statements\.csv', 'Assets:Neon:CHF')]


Syncing a fork
--------------

Details: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/syncing-a-fork

::

git remote add upstream https://github.com/tarioch/beancounttools.git
git remote -v
git fetch upstream
git checkout master
git merge upstream/master
git push
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ testing =
# And any other entry points, for example:
# pyscaffold.cli =
# awesome = pyscaffoldext.awesome.extension:AwesomeExtension
console_scripts =
nordigen-conf = tariochbctools.importers.nordigen.nordigen_config:run

[test]
# py.test options when running `python setup.py test`
Expand Down
65 changes: 65 additions & 0 deletions src/tariochbctools/importers/nordigen/importer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import yaml
from datetime import date
from os import path
import requests

from beancount.ingest import importer
from beancount.core import data
from beancount.core import amount
from beancount.core.number import D


class HttpServiceException(Exception):
pass


class Importer(importer.ImporterProtocol):
"""An importer for Nordigen API (e.g. for Revolut)."""

def identify(self, file):
return 'nordigen.yaml' == path.basename(file.name)

def file_account(self, file):
return ''

def extract(self, file, existing_entries):
with open(file.name, 'r') as f:
config = yaml.safe_load(f)
token = config['token']
headers = {'Authorization': 'Token ' + token}

entries = []
for account in config['accounts']:
accountId = account['id']
assetAccount = account['asset_account']
r = requests.get(f'https://ob.nordigen.com/api/accounts/{accountId}/transactions/', headers=headers)
try:
r.raise_for_status()
except requests.exceptions.HTTPError as e:
raise HttpServiceException(e, e.response.text)

transactions = sorted(r.json()['transactions']["booked"], key=lambda trx: trx['bookingDate'])
for trx in transactions:
metakv = {
'nordref': trx['transactionId'],
}
if 'currencyExchange' in trx:
instructedAmount = trx['currencyExchange']['instructedAmount']
metakv['original'] = instructedAmount['currency'] + ' ' + instructedAmount['amount']
meta = data.new_metadata('', 0, metakv)
trxDate = date.fromisoformat(trx['bookingDate'])
entry = data.Transaction(
meta,
trxDate,
'*',
'',
' '.join(trx['remittanceInformationUnstructuredArray']),
data.EMPTY_SET,
data.EMPTY_SET,
[
data.Posting(assetAccount, amount.Amount(D(str(trx['transactionAmount']['amount'])), trx['transactionAmount']['currency']), None, None, None, None),
]
)
entries.append(entry)

return entries
136 changes: 136 additions & 0 deletions src/tariochbctools/importers/nordigen/nordigen_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import argparse
import requests
import sys


def build_header(token):
return {'Authorization': 'Token ' + token}


def check_result(result):
try:
result.raise_for_status()
except requests.exceptions.HTTPError as e:
raise Exception(e, e.response.text)


def list_bank(token, country):
r = requests.get('https://ob.nordigen.com/api/aspsps/', params={'country': country}, headers=build_header(token))
check_result(r)

for asp in r.json():
print(asp['name'] + ': ' + asp['id'])


def create_link(token, userId, bank):
if not bank:
raise Exception('Please specify --bank it is required for create_link')
headers = build_header(token)
requisitionId = _find_requisition_id(token, userId)
if not requisitionId:
r = requests.post('https://ob.nordigen.com/api/requisitions/', data={
'redirect': 'http://localhost',
'enduser_id': userId,
'reference': userId,
}, headers=build_header(token))
check_result(r)
requisitionId = r.json()['id']

r = requests.post(f'https://ob.nordigen.com/api/requisitions/{requisitionId}/links/', data={'aspsp_id': bank}, headers=headers)
check_result(r)
link = r.json()['initiate']
print(f'Go to {link} for connecting to your bank.')


def list_accounts(token):
headers = build_header(token)
r = requests.get('https://ob.nordigen.com/api/requisitions/', headers=headers)
check_result(r)
for req in r.json()['results']:
print(req['enduser_id'] + ': ' + req['id'])
for account in req['accounts']:
ra = requests.get(f'https://ob.nordigen.com/api/accounts/{account}', headers=headers)
check_result(ra)
acc = ra.json()
asp = acc['aspsp_identifier']
iban = acc['iban']

ra = requests.get(f'https://ob.nordigen.com/api/accounts/{account}/details', headers=headers)
check_result(ra)
accDetails = ra.json()['account']

currency = accDetails['currency']
owner = accDetails['ownerName'] if 'ownerName' in accDetails else '-'
print(f'{account}: {asp} {owner} {iban} {currency}')


def delete_user(token, userId):
requisitionId = _find_requisition_id(token, userId)
if requisitionId:
r = requests.delete(f'https://ob.nordigen.com/api/requisitions/{requisitionId}', headers=build_header(token))
check_result(r)


def _find_requisition_id(token, userId):
headers = build_header(token)
r = requests.get('https://ob.nordigen.com/api/requisitions/', headers=headers)
check_result(r)
for req in r.json()['results']:
if req['enduser_id'] == userId:
return req['id']

return None


def parse_args(args):
parser = argparse.ArgumentParser(
description="nordigen-config"
)
parser.add_argument(
'--token',
required=True,
help='API Token, can be generated on Nordigen website',
)
parser.add_argument(
'--country',
default='GB',
help='Country Code for list_bank',
)
parser.add_argument(
'--userId',
default='beancount',
help='UserId for create_link and delete_user',
)
parser.add_argument(
'--bank',
help='Bank to connect to, see list_banks',
)
parser.add_argument(
'mode',
choices=[
'list_banks',
'create_link',
'list_accounts',
'delete_user',
],
)
return parser.parse_args(args)


def main(args):
args = parse_args(args)

if args.mode == 'list_banks':
list_bank(args.token, args.country)
elif args.mode == 'create_link':
create_link(args.token, args.userId, args.bank)
elif args.mode == 'list_accounts':
list_accounts(args.token)
elif args.mode == 'delete_user':
delete_user(args.token, args.userId)


def run():
"""Entry point for console_scripts
"""
main(sys.argv[1:])

0 comments on commit 3a3e5d2

Please sign in to comment.