Skip to content

Commit

Permalink
[fix] refactor standingorders into separate class
Browse files Browse the repository at this point in the history
  • Loading branch information
grindsa committed Dec 26, 2024
1 parent d488e42 commit 9fd2a50
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 102 deletions.
35 changes: 1 addition & 34 deletions dkb_robo/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,24 +680,7 @@ def _do_sso_redirect(self):
self.dkb_br = legacywrappper._new_instance(clientcookies)
self.logger.debug('api.Wrapper._do_sso_redirect() ended.\n')

def _filter_standing_orders(self, full_list: Dict[str, str]) -> List[Dict[str, str]]:
""" filter standing orders """
self.logger.debug('api.Wrapper._filter_standing_orders()\n')

so_list = []
if 'data' in full_list:
for ele in full_list['data']:
_tmp_dic = {
'amount': float(ele['attributes']['amount']['value']),
'currencycode': ele['attributes']['amount']['currencyCode'],
'purpose': ele['attributes']['description'],
'recpipient': ele['attributes']['creditor']['name'],
'creditoraccount': ele['attributes']['creditor']['creditorAccount'],
'interval': ele['attributes']['recurrence']}
so_list.append(_tmp_dic)

self.logger.debug('api.Wrapper._filter_standing_orders() ended\n')
return so_list


def _filter_transactions(self, transaction_list: List[Dict[str, str]], date_from: str, date_to: str, transaction_type: str) -> List[Dict[str, str]]:
""" filter transaction by date """
Expand Down Expand Up @@ -1140,22 +1123,6 @@ def get_credit_limits(self) -> Dict[str, str]:
self.logger.debug('api.Wrapper.get_credit_limits() ended\n')
return limit_dic

def get_standing_orders(self, uid: str = None):
""" get standing orders """
self.logger.debug('api.Wrapper.get_standing_orders()\n')

so_list = []
if uid:
response = self.client.get(self.base_url + self.api_prefix + '/accounts/payments/recurring-credit-transfers' + '?accountId=' + uid)
if response.status_code == 200:
_so_list = response.json()
so_list = self._filter_standing_orders(_so_list)
else:
raise DKBRoboError('get_standing_orders(): account-id is required')

self.logger.debug('api.Wrapper.get_standing_orders() ended\n')
return so_list

def _get_transaction_url(self, tr_dic):
""" get transaction url """
self.logger.debug('api.Wrapper._get_transaction_url()\n')
Expand Down
4 changes: 3 additions & 1 deletion dkb_robo/dkb_robo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# -*- coding: utf-8 -*-
from pathlib import Path
import time
from dkb_robo.standingorder import StandingOrder
from dkb_robo.postbox import PostBox
from dkb_robo.utilities import logger_setup, validate_dates, get_dateformat
from dkb_robo.api import Wrapper
Expand Down Expand Up @@ -80,7 +81,8 @@ def get_points(self):
def get_standing_orders(self, uid=None):
""" get standing orders """
self.logger.debug('DKBRobo.get_standing_orders()\n')
return self.wrapper.get_standing_orders(uid)
standingorder = StandingOrder(client=self.wrapper.client, logger=self.logger)
return standingorder.fetch(uid)

def get_transactions(self, transaction_url, atype, date_from, date_to, transaction_type='booked'):
""" exported method to get transactions """
Expand Down
56 changes: 56 additions & 0 deletions dkb_robo/standingorder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
""" Module for handling dkb standing orders """
from typing import Dict, List
import logging
import requests
from dkb_robo.api import DKBRoboError


class StandingOrder:
""" StandingOrder class """
def __init__(self, client: requests.Session, logger: logging.Logger, base_url: str = 'https://banking.dkb.de/api'):

This comment has been minimized.

Copy link
@vanto

vanto Dec 28, 2024

Contributor

I think it is not needed to pass the logger instance around. Instead, you can just create an instance per module(logger = logging.getLogger(__name__)) and even could configure the logging formatter to output the module/logger name -- then you don't need to prefix the log messages anymore. https://docs.python.org/3/howto/logging.html#configuring-logging

This comment has been minimized.

Copy link
@grindsa

grindsa Dec 30, 2024

Author Owner

I would like to have the ability to turn on debug logging by setting a corresponding flag (debug=True) during initialization. Having logger instantiate by module would require passing the debug flag around the different classes. Maybe that's a better option for the reason you mentioned.

This comment has been minimized.

Copy link
@vanto

vanto Dec 30, 2024

Contributor

Understood. Actually, it should be possible to set the log level at the root level during setup and all subsequently created loggers should inherit that setting.

I think logging.basicConfig(level=logging.DEBUG) in https://github.com/grindsa/dkb-robo/blob/master/dkb_robo/utilities.py#L77 should do that already. I cannot try it myself right now, but I'm pretty sure that this is a static setting. The log_format variable could be set to %(module)s: %(message)s in the debug case in order to get the module name in the output as well.

This comment has been minimized.

Copy link
@grindsa

grindsa Jan 1, 2025

Author Owner

Thanks for pointing this out. I changed the logging implementation with c28c87b as you suggested. Works like a charm.

self.client = client
self.logger = logger
self.base_url = base_url
self.uid = None

def _filter(self, full_list: Dict[str, str]) -> List[Dict[str, str]]:
""" filter standing orders """
self.logger.debug('api.StandingOrder._filter()\n')

so_list = []
if 'data' in full_list:
for ele in full_list['data']:

try:
amount = float(ele.get('attributes', {}).get('amount', {}).get('value', None))
except Exception as err:
self.logger.error('api.StandingOrder._filter() error: %s', err)
amount = None

_tmp_dic = {
'amount': amount,
'currencycode': ele.get('attributes', {}).get('amount', {}).get('currencyCode', None),
'purpose': ele.get('attributes', {}).get('description', None),
'recpipient': ele.get('attributes', {}).get('creditor', {}).get('name', None),
'creditoraccount': ele.get('attributes', {}).get('creditor', {}).get('creditorAccount', None),
'interval': ele.get('attributes', {}).get('recurrence', None)}
so_list.append(_tmp_dic)

self.logger.debug('api.StandingOrder._filter() ended with: %s entries.', len(so_list))
return so_list

def fetch(self, uid) -> Dict:
""" fetch standing orders """
self.logger.debug('api.Standorder.get()\n')

so_list = []
if uid:
response = self.client.get(self.base_url + '/accounts/payments/recurring-credit-transfers' + '?accountId=' + uid)
if response.status_code == 200:
_so_list = response.json()
so_list = self._filter(_so_list)
else:
raise DKBRoboError('get_standing_orders(): account-id is required')

self.logger.debug('api.Wrapper.get_standing_orders() ended\n')
return so_list
67 changes: 0 additions & 67 deletions test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1385,79 +1385,12 @@ def test_111_get_credit_limits(self):
self.dkb.account_dic = {'foo': 'bar'}
self.assertFalse(self.dkb.get_credit_limits())

@patch('dkb_robo.api.Wrapper._filter_standing_orders')
def test_112___get_standing_orders(self, mock_filter):
""" test _get_standing_orders() """
with self.assertRaises(Exception) as err:
self.assertFalse(self.dkb.get_standing_orders())
self.assertEqual('get_standing_orders(): account-id is required', str(err.exception))
self.assertFalse(mock_filter.called)

@patch('dkb_robo.api.Wrapper._filter_standing_orders')
def test_113___get_standing_orders(self, mock_filter):
""" test _get_standing_orders() """
self.dkb.client = Mock()
self.dkb.client.get.return_value.status_code = 400
self.dkb.client.get.return_value.json.return_value = {'foo': 'bar'}
self.assertFalse(self.dkb.get_standing_orders(uid='uid'))
self.assertFalse(mock_filter.called)

@patch('dkb_robo.api.Wrapper._filter_standing_orders')
def test_114___get_standing_orders(self, mock_filter):
""" test _get_standing_orders() """
self.dkb.client = Mock()
self.dkb.client.get.return_value.status_code = 200
self.dkb.client.get.return_value.json.return_value = {'foo': 'bar'}
mock_filter.return_value = 'mock_filter'
self.assertEqual('mock_filter', self.dkb.get_standing_orders(uid='uid'))
self.assertTrue(mock_filter.called)

def test_115__filter_standing_orders(self):
""" test _filter_standing_orders() """
full_list = {}
self.assertFalse(self.dkb._filter_standing_orders(full_list))

def test_116__filter_standing_orders(self):
""" test _filter_standing_orders() """
full_list = {
"data": [
{
"attributes": {
"description": "description",
"amount": {
"currencyCode": "EUR",
"value": "100.00"
},
"creditor": {
"name": "cardname",
"creditorAccount": {
"iban": "crediban",
"bic": "credbic"
}
},
"recurrence": {
"from": "2020-01-01",
"until": "2025-12-01",
"frequency": "monthly",
"nextExecutionAt": "2020-02-01"
}
}
}]}
result = [{'amount': 100.0, 'currencycode': 'EUR', 'purpose': 'description', 'recpipient': 'cardname', 'creditoraccount': {'iban': 'crediban', 'bic': 'credbic'}, 'interval': {'from': '2020-01-01', 'until': '2025-12-01', 'frequency': 'monthly', 'nextExecutionAt': '2020-02-01'}}]
self.assertEqual(result, self.dkb._filter_standing_orders(full_list))

def test_117_add_cardlimit(self):
""" test _add_cardlimit() """
card = {'attributes': {'expiryDate': 'expiryDate', 'limit': {'value': 'value', 'foo': 'bar'}}}
result = {'expirydate': 'expiryDate', 'limit': 'value'}
self.assertEqual(result, self.dkb._add_cardlimit(card))

def test_118_filter_standing_orders(self):
""" e2e get_standing_orders() """
so_list = json_load(self.dir_path + '/mocks/so.json')
result = [{'amount': 100.0, 'currencycode': 'EUR', 'purpose': 'description1', 'recpipient': 'name1', 'creditoraccount': {'iban': 'iban1', 'bic': 'bic1'}, 'interval': {'from': '2022-01-01', 'until': '2025-12-01', 'frequency': 'monthly', 'holidayExecutionStrategy': 'following', 'nextExecutionAt': '2022-11-01'}}, {'amount': 200.0, 'currencycode': 'EUR', 'purpose': 'description2', 'recpipient': 'name2', 'creditoraccount': {'iban': 'iban2', 'bic': 'bic2'}, 'interval': {'from': '2022-02-01', 'until': '2025-12-02', 'frequency': 'monthly', 'holidayExecutionStrategy': 'following', 'nextExecutionAt': '2022-11-02'}}, {'amount': 300.0, 'currencycode': 'EUR', 'purpose': 'description3', 'recpipient': 'name3', 'creditoraccount': {'iban': 'iban3', 'bic': 'bic3'}, 'interval': {'from': '2022-03-01', 'until': '2025-03-01', 'frequency': 'monthly', 'holidayExecutionStrategy': 'following', 'nextExecutionAt': '2022-03-01'}}]
self.assertEqual(result, self.dkb._filter_standing_orders(so_list))

def test_119_logout(self):
""" test logout """
self.assertFalse(self.dkb.logout())
Expand Down
136 changes: 136 additions & 0 deletions test/test_standingorder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# pylint: disable=r0904, c0415, c0413, r0913, w0212
""" unittests for dkb_robo """
import sys
import os
from datetime import date
import unittest
import logging
import json
from unittest.mock import patch, Mock, MagicMock, mock_open
from bs4 import BeautifulSoup
from mechanicalsoup import LinkNotFoundError
import io
sys.path.insert(0, '.')
sys.path.insert(0, '..')
from dkb_robo.standingorder import StandingOrder

def json_load(fname):
""" simple json load """

with open(fname, 'r', encoding='utf8') as myfile:
data_dic = json.load(myfile)

return data_dic


class TestDKBRobo(unittest.TestCase):
""" test class """

@patch("requests.Session")
def setUp(self, mock_session):
self.dir_path = os.path.dirname(os.path.realpath(__file__))
self.logger = logging.getLogger('dkb_robo')
self.dkb = StandingOrder(logger=self.logger, client=mock_session)

@patch('dkb_robo.standingorder.StandingOrder._filter')
def test_001_fetch(self, mock_filter):
""" test StandingOrder.fetch() without uid """
self.dkb.client = Mock()
self.dkb.client.get.return_value.status_code = 200
self.dkb.client.get.return_value.json.return_value = {'foo': 'bar'}

with self.assertRaises(Exception) as err:
self.assertFalse(self.dkb.fetch(None))
self.assertEqual('get_standing_orders(): account-id is required', str(err.exception))
self.assertFalse(mock_filter.called)

@patch('dkb_robo.standingorder.StandingOrder._filter')
def test_002_fetch(self, mock_filter):
""" test StandingOrder.fetch() with uid but http error """
self.dkb.client = Mock()
self.dkb.client.get.return_value.status_code = 400
self.dkb.client.get.return_value.json.return_value = {'foo': 'bar'}
self.assertFalse(self.dkb.fetch(uid='uid'))
self.assertFalse(mock_filter.called)

@patch('dkb_robo.standingorder.StandingOrder._filter')
def test_003_fetch(self, mock_filter):
""" test StandingOrder.fetch() with uid no error """
self.dkb.client = Mock()
self.dkb.client.get.return_value.status_code = 200
self.dkb.client.get.return_value.json.return_value = {'foo': 'bar'}
mock_filter.return_value = 'mock_filter'
self.assertEqual('mock_filter', self.dkb.fetch(uid='uid'))
self.assertTrue(mock_filter.called)

def test_004__filter(self):
""" test StandingOrder._filter() with empty list """
full_list = {}
self.assertFalse(self.dkb._filter(full_list))

def test_005__filter(self):
""" test StandingOrder._filter() with list """
full_list = {
"data": [
{
"attributes": {
"description": "description",
"amount": {
"currencyCode": "EUR",
"value": "100.00"
},
"creditor": {
"name": "cardname",
"creditorAccount": {
"iban": "crediban",
"bic": "credbic"
}
},
"recurrence": {
"from": "2020-01-01",
"until": "2025-12-01",
"frequency": "monthly",
"nextExecutionAt": "2020-02-01"
}
}
}]}
result = [{'amount': 100.0, 'currencycode': 'EUR', 'purpose': 'description', 'recpipient': 'cardname', 'creditoraccount': {'iban': 'crediban', 'bic': 'credbic'}, 'interval': {'from': '2020-01-01', 'until': '2025-12-01', 'frequency': 'monthly', 'nextExecutionAt': '2020-02-01'}}]
self.assertEqual(result, self.dkb._filter(full_list))

def test_006__filter(self):
""" test StandingOrder._filter() with list from file """
so_list = json_load(self.dir_path + '/mocks/so.json')
result = [{'amount': 100.0, 'currencycode': 'EUR', 'purpose': 'description1', 'recpipient': 'name1', 'creditoraccount': {'iban': 'iban1', 'bic': 'bic1'}, 'interval': {'from': '2022-01-01', 'until': '2025-12-01', 'frequency': 'monthly', 'holidayExecutionStrategy': 'following', 'nextExecutionAt': '2022-11-01'}}, {'amount': 200.0, 'currencycode': 'EUR', 'purpose': 'description2', 'recpipient': 'name2', 'creditoraccount': {'iban': 'iban2', 'bic': 'bic2'}, 'interval': {'from': '2022-02-01', 'until': '2025-12-02', 'frequency': 'monthly', 'holidayExecutionStrategy': 'following', 'nextExecutionAt': '2022-11-02'}}, {'amount': 300.0, 'currencycode': 'EUR', 'purpose': 'description3', 'recpipient': 'name3', 'creditoraccount': {'iban': 'iban3', 'bic': 'bic3'}, 'interval': {'from': '2022-03-01', 'until': '2025-03-01', 'frequency': 'monthly', 'holidayExecutionStrategy': 'following', 'nextExecutionAt': '2022-03-01'}}]
self.assertEqual(result, self.dkb._filter(so_list))

def test_007__filter(self):
""" test StandingOrder._filter() with incomplete list """
full_list = {
"data": [
{
"attributes": {
"description": "description",
"creditor": {
"name": "cardname",
"creditorAccount": {
"iban": "crediban",
"bic": "credbic"
}
},
"recurrence": {
"from": "2020-01-01",
"until": "2025-12-01",
"frequency": "monthly",
"nextExecutionAt": "2020-02-01"
}
}
}]}
result = [{'amount': None, 'currencycode': None, 'purpose': 'description', 'recpipient': 'cardname', 'creditoraccount': {'iban': 'crediban', 'bic': 'credbic'}, 'interval': {'from': '2020-01-01', 'until': '2025-12-01', 'frequency': 'monthly', 'nextExecutionAt': '2020-02-01'}}]
with self.assertLogs('dkb_robo', level='INFO') as lcm:
self.assertEqual(result, self.dkb._filter(full_list))
self.assertIn("ERROR:dkb_robo:api.StandingOrder._filter() error: float() argument must be a string or a real number, not 'NoneType'", lcm.output)

if __name__ == '__main__':

unittest.main()

0 comments on commit 9fd2a50

Please sign in to comment.