diff --git a/README.rst b/README.rst
index 4fe2caa..131de83 100644
--- a/README.rst
+++ b/README.rst
@@ -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 `_
-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 `_
-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 `_
-
-::
-
- 2019-01-01 commodity BTC
- price: "EUR:tariochbctools.plugins.prices.bitstamp/BTC"
-
-**exchangeratesapi**
-
-Fetches prices from `ratesapi.io `_
-
-::
-
- 2019-01-01 commodity EUR
- price: "CHF:tariochbctools.plugins.prices.exchangeratesapi/EUR"
+Also see `Beanprice `_
**interactivebrokers**
@@ -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 `_
-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
---------
@@ -161,6 +115,37 @@ Create a file called truelayer.yaml in your import location (e.g. download folde
client_secret:
refresh_token:
+**Nordigen**
+
+Import from `Nordigen `_ 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:
+
+ accounts:
+ - id:
+ asset_account: "Assets:MyAccount:CHF"
+
+
**zkb**
Import mt940 from `Zürcher Kantonalbank `_
@@ -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 `_
+Import PDF from `Bank Cler ZAK `_
+
+::
+
+ from tariochbctools.importers.zak import importer as zakimp
+ CONFIG = [ zakimp.Importer(r'Kontoauszug.*\.pdf', 'Assets:ZAK:CHF') ]
**mt940**
@@ -283,18 +273,3 @@ Import CSV from `Neon `_
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
diff --git a/setup.cfg b/setup.cfg
index 696ae81..b6621fc 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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`
diff --git a/src/tariochbctools/importers/nordigen/importer.py b/src/tariochbctools/importers/nordigen/importer.py
new file mode 100644
index 0000000..2af7806
--- /dev/null
+++ b/src/tariochbctools/importers/nordigen/importer.py
@@ -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
diff --git a/src/tariochbctools/importers/nordigen/nordigen_config.py b/src/tariochbctools/importers/nordigen/nordigen_config.py
new file mode 100644
index 0000000..5291d93
--- /dev/null
+++ b/src/tariochbctools/importers/nordigen/nordigen_config.py
@@ -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:])