Skip to content

Commit

Permalink
separated requisition handling and improved startdate feature
Browse files Browse the repository at this point in the history
  • Loading branch information
dnbasta committed Mar 3, 2024
1 parent 3234ca8 commit c66a7e0
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 92 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ ynab_api_import = YnabApiImport(resource_id='<resource_id>',
budget_id='<budget_id>',
account_id='<account_id>')
```
### Delete current bank authorization
By default you can create only one bank authorization per reference. If you need to replace the authorization under your
current reference you can explicitly do that by calling the following function.
```py
ynab_api_import.delete_currrent_auth()
```
### Show Logs
The library logs information about the result of the methods on the 'INFO' level. If you want to see these logs
import the logging module and set it to the level `INFO`. You can also access the logger for advanced configuration
Expand Down
49 changes: 49 additions & 0 deletions tests/test_requisitionhandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from unittest.mock import MagicMock

import pytest

from ynabapiimport.models.exceptions import NoRequisitionError, ReferenceNotUnique
from ynabapiimport.requisitionhandler import RequisitionHandler


@pytest.mark.parametrize('test_input', [[], [{'reference': 'xxx::xxx', 'status': 'LN'}]])
def test_fetch_requisition_fail(test_input):
# Arrange
client = MagicMock()
client.requisition.get_requisitions.return_value = {'results': test_input}

# Act & Assert
rh = RequisitionHandler(client=client, reference='reference')
with pytest.raises(NoRequisitionError):
r = rh.fetch_requisition()


def test_fetch_requisition_success():
# Arrange
client = MagicMock()
mock_req = {'reference': 'reference::xxx', 'status': 'LN'}
client.requisition.get_requisitions.return_value = {'results': [mock_req]}

# Act
rh = RequisitionHandler(client=client, reference='reference')
r = rh.fetch_requisition()

# Assert
assert r == mock_req


def test_reference_is_unique_fail():
# Arrange
req_list = [{'reference': 'xxx::xxx'}, {'reference': 'ref::xxx'}]

# Act & Assert
rh = RequisitionHandler(client=MagicMock(), reference='ref')
with pytest.raises(ReferenceNotUnique):
rh.reference_is_unique(req_list)


@pytest.mark.parametrize('test_input', [[], [{'reference': 'xxx:xxx'}]])
def test_reference_is_unique_success(test_input):
# Act
rh = RequisitionHandler(client=MagicMock(), reference='ref')
rh.reference_is_unique(test_input)
15 changes: 4 additions & 11 deletions ynabapiimport/accountfetcher.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from typing import List

from nordigen import NordigenClient
from requests import HTTPError

from ynabapiimport.models.exceptions import NoAccountError, MultipleAccountsError, NoRequisitionError
from ynabapiimport.models.exceptions import NoAccountError, MultipleAccountsError
from ynabapiimport.requisitionhandler import RequisitionHandler


class AccountFetcher:

def __init__(self, client: NordigenClient, reference: str):
self._client = client
self._reference = reference
self._requisitionhandler = RequisitionHandler(reference=reference, client=client)

@staticmethod
def fetch_by_resource_id(resource_id: str, account_dicts: List[dict]) -> str:
Expand All @@ -20,7 +20,7 @@ def fetch_by_resource_id(resource_id: str, account_dicts: List[dict]) -> str:
raise NoAccountError(f"No active account with resource_id. Available accounts: {account_dicts}")

def fetch(self, resource_id: str = None) -> str:
req = self.fetch_requisition()
req = self._requisitionhandler.fetch_requisition()

account_dicts = [{**self._client.account_api(id=a).get_details()['account'],
**{'account_id': a}} for a in req['accounts']]
Expand All @@ -33,10 +33,3 @@ def fetch(self, resource_id: str = None) -> str:
raise MultipleAccountsError(f"There are multiple active accounts available in active requisition. "
f"Please provide one of the following resourceId when initializing library.", account_dicts)
return next(a['account_id'] for a in account_dicts)

def fetch_requisition(self) -> dict:
try:
results = self._client.requisition.get_requisitions()['results']
return next(r for r in results if r['status'] == 'LN' and r['reference'].split('::')[0] == self._reference)
except (HTTPError, StopIteration):
raise NoRequisitionError()
67 changes: 0 additions & 67 deletions ynabapiimport/gocardlessclient.py

This file was deleted.

58 changes: 58 additions & 0 deletions ynabapiimport/requisitionhandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import List
from urllib.error import HTTPError
from uuid import uuid4

from nordigen import NordigenClient

from ynabapiimport.models.exceptions import NoRequisitionError, ReferenceNotUnique, ReferenceNotValid


class RequisitionHandler:

def __init__(self, client: NordigenClient, reference: str):
self._client = client
self._reference = reference

def fetch_requisition(self) -> dict:
try:
results = self._client.requisition.get_requisitions()['results']
return next(r for r in results if r['status'] == 'LN' and r['reference'].split('::')[0] == self._reference)
except (HTTPError, StopIteration):
raise NoRequisitionError()

def create_requisition_auth_link(self, institution_id: str) -> str:
req_list = self._client.requisition.get_requisitions()['results']

self.delete_inactive_requisitions(req_list=req_list)
self.reference_is_unique(req_list=req_list)
self.reference_is_valid()

init_session = self._client.initialize_session(institution_id=institution_id,
redirect_uri='http://localhost:',
reference_id=f"{self._reference}::{uuid4()}",
max_historical_days=730)
return init_session.link

def delete_inactive_requisitions(self, req_list: List[dict]):
inactive_requisitions = [r['id'] for r in req_list
if r['status'] != 'LN' and r['reference'].split('::')[0] == self._reference]
[self._client.requisition.delete_requisition(ir) for ir in inactive_requisitions]

def delete_current_requisition(self):
req = self.fetch_requisition()
self._client.requisition.delete_requisition(requisition_id=req['id'])

def reference_is_unique(self, req_list: List[dict]):
try:
existing_reference = next(r for r in req_list if r['reference'].split('::')[0] == self._reference)
raise ReferenceNotUnique(f"{self._reference} already used for: {existing_reference}")
except StopIteration:
pass

def reference_is_valid(self):
if '::' in self._reference:
raise ReferenceNotValid(f"'{self._reference}' is not valid: '::' is not allowed")

def get_institutions(self, countrycode: str) -> List[dict]:
institutions = self._client.institution.get_institutions(countrycode)
return [{'institution_id': i['id'], 'name': i['name']} for i in institutions]
28 changes: 28 additions & 0 deletions ynabapiimport/transactionfetcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from datetime import date
from typing import List

from nordigen import NordigenClient

from ynabapiimport.accountfetcher import AccountFetcher
from ynabapiimport.models.transaction import Transaction


class TransactionFetcher:
def __init__(self, client: NordigenClient, reference: str, resource_id: str):
self._client = client
self._reference = reference
self.resource_id = resource_id

def fetch(self, startdate: date) -> List[Transaction]:
af = AccountFetcher(client=self._client, reference=self._reference)

if self.resource_id:
account_id = af.fetch(resource_id=self.resource_id)
else:
account_id = af.fetch()

account = self._client.account_api(id=account_id)

transaction_dicts = account.get_transactions(date_from=date.strftime(startdate, '%Y-%m-%d'))
transactions = [Transaction.from_dict(t) for t in transaction_dicts['transactions']['booked']]
return transactions
42 changes: 28 additions & 14 deletions ynabapiimport/ynabapiimport.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import json
import logging
from datetime import date
from datetime import date, timedelta
from pathlib import Path
from typing import List

import yaml
from nordigen import NordigenClient

from ynabapiimport.gocardlessclient import GocardlessClient
from ynabapiimport.requisitionhandler import RequisitionHandler
from ynabapiimport.transactionfetcher import TransactionFetcher
from ynabapiimport.memocleaner import MemoCleaner
from ynabapiimport.ynabclient import YnabClient

Expand All @@ -16,11 +17,11 @@ class YnabApiImport:
def __init__(self, secret_id: str, secret_key: str, token: str,
reference: str, budget_id: str, account_id: str, resource_id: str = None) -> None:
self.logger = self._set_up_logger()
self._gocardless_client = GocardlessClient(secret_id=secret_id,
secret_key=secret_key,
reference=reference,
resource_id=resource_id)
self._reference = reference
self._resource_id = resource_id
self._ynab_client = YnabClient(token=token, account_id=account_id, budget_id=budget_id)
self._gocardless_client = NordigenClient(secret_id=secret_id, secret_key=secret_key)
self._gocardless_client.generate_token()

@classmethod
def from_yaml(cls, path: str):
Expand All @@ -40,31 +41,44 @@ def from_yaml(cls, path: str):
resource_id=resource_id)

def import_transactions(self, startdate: date = None, memo_regex: str = None) -> int:
transactions = self._gocardless_client.fetch_transactions(startdate=startdate)
if startdate is None:
startdate = date.today() - timedelta(days=90)

tf = TransactionFetcher(client=self._gocardless_client, reference=self._reference,
resource_id=self._resource_id)
transactions = tf.fetch(startdate=startdate)

if memo_regex:
mc = MemoCleaner(memo_regex=memo_regex)
transactions = [mc.clean(t) for t in transactions]

i = self._ynab_client.insert(transactions)
self.logger.info(f"inserted {i} transactions for {self._gocardless_client.reference}")
self.logger.info(f"inserted {i} transactions for {self._reference}")
return i

def create_auth_link(self, institution_id: str) -> str:
auth_link = self._gocardless_client.create_requisition_auth_link(institution_id=institution_id)
self.logger.info(f'created auth link for {institution_id} under reference {self._gocardless_client.reference}')
rh = RequisitionHandler(client=self._gocardless_client, reference=self._reference)
auth_link = rh.create_requisition_auth_link(institution_id=institution_id)
self.logger.info(f'created auth link for {institution_id} under reference {self._reference}')
return auth_link

def delete_current_auth(self):
rh = RequisitionHandler(client=self._gocardless_client, reference=self._reference)
rh.delete_current_requisition()
self.logger.info(f'deleted auth for reference {self._reference}')

def fetch_institutions(self, countrycode: str) -> List[dict]:
institutions = self._gocardless_client.get_institutions(countrycode=countrycode)
rh = RequisitionHandler(client=self._gocardless_client, reference=self._reference)
institutions = rh.get_institutions(countrycode=countrycode)
self.logger.info(f'fetched list with {len(institutions)} institutions for countrycode {countrycode}')
return institutions

def test_memo_regex(self, memo_regex: str) -> List[dict]:
transactions = self._gocardless_client.fetch_transactions()
tf = TransactionFetcher(client=self._gocardless_client, reference=self._reference, resource_id=self._resource_id)
transactions = tf.fetch(date.today() - timedelta(days=90))
mc = MemoCleaner(memo_regex=memo_regex)
r = [{t.memo: mc.clean(t).memo} for t in transactions]
self.logger.info(f'tested memo regex on {len(r)} transactions from {self._gocardless_client.reference}')
self.logger.info(f'tested memo regex on {len(r)} transactions from {self._reference}')
return r

@staticmethod
Expand Down

0 comments on commit c66a7e0

Please sign in to comment.