Get the PSD2 client under testing
Having a comprehension unit testing allows for peace of mind refactoring. Changed the PSD2Client constructor and extract method to accept credentials and banks, respectively. Create Exception tree for the Extract module. Adds mocks to the NordigenClient requests' methods.
This commit is contained in:
parent
184628dcfc
commit
eb5c1781f0
@ -1,6 +1,8 @@
|
||||
import csv
|
||||
import dotenv
|
||||
from pathlib import Path
|
||||
import pickle
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
from pfbudget.common.types import Operation
|
||||
@ -25,9 +27,13 @@ from pfbudget.db.model import (
|
||||
Transaction,
|
||||
TransactionCategory,
|
||||
)
|
||||
from pfbudget.extract.credentials import Credentials
|
||||
from pfbudget.extract.extract import Extract
|
||||
from pfbudget.extract.psd2 import PSD2Client
|
||||
from pfbudget.extract.parsers import parse_data
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
class Manager:
|
||||
def __init__(self, db: str, verbosity: int = 0):
|
||||
@ -72,16 +78,16 @@ class Manager:
|
||||
session.add(sorted(transactions))
|
||||
|
||||
case Operation.Download:
|
||||
client = PSD2Client()
|
||||
client = Manager.nordigen_client()
|
||||
with self.db.session() as session:
|
||||
if len(params[3]) == 0:
|
||||
client.banks = session.get(Bank, Bank.nordigen)
|
||||
banks = session.get(Bank, Bank.nordigen)
|
||||
else:
|
||||
client.banks = session.get(Bank, Bank.name, params[3])
|
||||
banks = session.get(Bank, Bank.name, params[3])
|
||||
session.expunge_all()
|
||||
client.start = params[0]
|
||||
client.end = params[1]
|
||||
transactions = client.parse()
|
||||
transactions = client.extract(banks)
|
||||
|
||||
# dry-run
|
||||
if not params[2]:
|
||||
@ -116,15 +122,15 @@ class Manager:
|
||||
session.remove_by_name(Nordigen, params)
|
||||
|
||||
case Operation.Token:
|
||||
PSD2Client().token()
|
||||
Manager.nordigen_client().generate_token()
|
||||
|
||||
case Operation.RequisitionId:
|
||||
link, _ = PSD2Client().requisition(params[0], params[1])
|
||||
link, _ = Manager.nordigen_client().requisition(params[0], params[1])
|
||||
print(f"Opening {link} to request access to {params[0]}")
|
||||
webbrowser.open(link)
|
||||
|
||||
case Operation.PSD2CountryBanks:
|
||||
banks = PSD2Client().country_banks(params[0])
|
||||
banks = Manager.nordigen_client().country_banks(params[0])
|
||||
print(banks)
|
||||
|
||||
case (
|
||||
@ -411,3 +417,12 @@ class Manager:
|
||||
@db.setter
|
||||
def db(self, url: str):
|
||||
self._db = url
|
||||
|
||||
@staticmethod
|
||||
def nordigen_client() -> Extract:
|
||||
credentials = Credentials(
|
||||
os.environ.get("SECRET_ID"),
|
||||
os.environ.get("SECRET_KEY"),
|
||||
os.environ.get("TOKEN"),
|
||||
)
|
||||
return PSD2Client(credentials)
|
||||
|
||||
11
pfbudget/extract/credentials.py
Normal file
11
pfbudget/extract/credentials.py
Normal file
@ -0,0 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Credentials:
|
||||
id: str
|
||||
key: str
|
||||
token: str = ""
|
||||
|
||||
def valid(self) -> bool:
|
||||
return self.id and self.key
|
||||
10
pfbudget/extract/exceptions.py
Normal file
10
pfbudget/extract/exceptions.py
Normal file
@ -0,0 +1,10 @@
|
||||
class ExtractError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BankError(ExtractError):
|
||||
pass
|
||||
|
||||
|
||||
class CredentialsError(ExtractError):
|
||||
pass
|
||||
@ -5,5 +5,5 @@ from pfbudget.db.model import Transaction
|
||||
|
||||
class Extract(ABC):
|
||||
@abstractmethod
|
||||
def parse(self) -> list[Transaction]:
|
||||
def extract(self) -> list[Transaction]:
|
||||
return NotImplementedError
|
||||
|
||||
@ -1,75 +1,55 @@
|
||||
import datetime as dt
|
||||
import dotenv
|
||||
import json
|
||||
import nordigen
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
import pfbudget.db.model as t
|
||||
from pfbudget.utils.converters import convert
|
||||
|
||||
from .credentials import Credentials
|
||||
from .exceptions import BankError, CredentialsError
|
||||
from .extract import Extract
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
class PSD2Client(Extract):
|
||||
redirect_url = "https://murta.dev"
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, credentials: Credentials):
|
||||
super().__init__()
|
||||
|
||||
if not (key := os.environ.get("SECRET_KEY")) or not (
|
||||
id := os.environ.get("SECRET_ID")
|
||||
):
|
||||
raise
|
||||
if not credentials.valid():
|
||||
raise CredentialsError
|
||||
|
||||
self._client = nordigen.NordigenClient(
|
||||
secret_key=key,
|
||||
secret_id=id,
|
||||
secret_key=credentials.key, secret_id=credentials.id, timeout=5
|
||||
)
|
||||
|
||||
self._client.token = self.__token()
|
||||
if credentials.token:
|
||||
self._token = credentials.token
|
||||
|
||||
self._start = dt.date.min
|
||||
self._end = dt.date.max
|
||||
|
||||
def parse(self) -> list[t.BankTransaction]:
|
||||
def extract(self, banks: Sequence[t.Bank]) -> list[t.BankTransaction]:
|
||||
transactions = []
|
||||
assert len(self._banks) > 0
|
||||
if not banks or any(not b.nordigen for b in banks):
|
||||
raise BankError
|
||||
|
||||
for bank in self._banks:
|
||||
print(f"Downloading from {bank}...")
|
||||
requisition = self.client.requisition.get_requisition_by_id(
|
||||
bank.nordigen.requisition_id
|
||||
)
|
||||
for bank in banks:
|
||||
downloaded = None
|
||||
try:
|
||||
print(f"Downloading from {bank}...")
|
||||
downloaded = self.download(bank.nordigen.requisition_id)
|
||||
except requests.HTTPError as e:
|
||||
print(f"There was an issue downloading from {bank.name} -> {e}")
|
||||
continue
|
||||
|
||||
print(requisition)
|
||||
for acc in requisition["accounts"]:
|
||||
account = self._client.account_api(acc)
|
||||
|
||||
retries = 0
|
||||
downloaded = {}
|
||||
while retries < 3:
|
||||
try:
|
||||
downloaded = account.get_transactions()
|
||||
break
|
||||
except requests.ReadTimeout:
|
||||
retries += 1
|
||||
print(f"Request #{retries} timed-out, retrying in 1s")
|
||||
time.sleep(1)
|
||||
except requests.HTTPError as e:
|
||||
retries += 1
|
||||
print(f"Request #{retries} failed with {e}, retrying in 1s")
|
||||
time.sleep(1)
|
||||
|
||||
if not downloaded:
|
||||
print(f"Couldn't download transactions for {account}")
|
||||
continue
|
||||
|
||||
with open("json/" + bank.name + ".json", "w") as f:
|
||||
json.dump(downloaded, f)
|
||||
if downloaded:
|
||||
self.dump(bank, downloaded)
|
||||
|
||||
converted = [
|
||||
convert(t, bank) for t in downloaded["transactions"]["booked"]
|
||||
@ -81,10 +61,40 @@ class PSD2Client(Extract):
|
||||
|
||||
return sorted(transactions)
|
||||
|
||||
def token(self):
|
||||
token = self._client.generate_token()
|
||||
print(f"New access token: {token}")
|
||||
return token
|
||||
def download(self, requisition_id):
|
||||
requisition = self._client.requisition.get_requisition_by_id(requisition_id)
|
||||
print(requisition)
|
||||
|
||||
transactions = {}
|
||||
for acc in requisition["accounts"]:
|
||||
account = self._client.account_api(acc)
|
||||
|
||||
retries = 0
|
||||
while retries < 3:
|
||||
try:
|
||||
downloaded = account.get_transactions()
|
||||
break
|
||||
except requests.ReadTimeout:
|
||||
retries += 1
|
||||
print(f"Request #{retries} timed-out, retrying in 1s")
|
||||
time.sleep(1)
|
||||
|
||||
if not downloaded:
|
||||
print(f"Couldn't download transactions for {account}")
|
||||
continue
|
||||
|
||||
transactions.update(downloaded)
|
||||
|
||||
return transactions
|
||||
|
||||
def dump(self, bank, downloaded):
|
||||
with open("json/" + bank.name + ".json", "w") as f:
|
||||
json.dump(downloaded, f)
|
||||
|
||||
def generate_token(self):
|
||||
self.token = self._client.generate_token()
|
||||
print(f"New access token: {self.token}")
|
||||
return self.token
|
||||
|
||||
def requisition(self, id: str, country: str = "PT"):
|
||||
requisition = self._client.initialize_session(
|
||||
@ -97,18 +107,6 @@ class PSD2Client(Extract):
|
||||
def country_banks(self, country: str):
|
||||
return self._client.institution.get_institutions(country)
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def banks(self):
|
||||
return self._banks
|
||||
|
||||
@banks.setter
|
||||
def banks(self, value):
|
||||
self._banks = value
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self._start
|
||||
@ -125,10 +123,20 @@ class PSD2Client(Extract):
|
||||
def end(self, value):
|
||||
self._end = value
|
||||
|
||||
def __token(self):
|
||||
if token := os.environ.get("TOKEN"):
|
||||
return token
|
||||
else:
|
||||
token = self._client.generate_token()
|
||||
print(f"New access token: {token}")
|
||||
return token["access"]
|
||||
# def __token(self):
|
||||
# if token := os.environ.get("TOKEN"):
|
||||
# return token
|
||||
# else:
|
||||
# token = self._client.generate_token()
|
||||
# print(f"New access token: {token}")
|
||||
# return token["access"]
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
return self._token
|
||||
|
||||
@token.setter
|
||||
def token(self, value):
|
||||
if self._token:
|
||||
print("Replacing existing token with {value}")
|
||||
self._token = value
|
||||
|
||||
172
poetry.lock
generated
172
poetry.lock
generated
@ -264,6 +264,73 @@ mypy = ["contourpy[bokeh]", "docutils-stubs", "mypy (==0.991)", "types-Pillow"]
|
||||
test = ["Pillow", "matplotlib", "pytest"]
|
||||
test-no-images = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.2.3"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"},
|
||||
{file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"},
|
||||
{file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"},
|
||||
{file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"},
|
||||
{file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"},
|
||||
{file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"},
|
||||
{file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"},
|
||||
{file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "cycler"
|
||||
version = "0.11.0"
|
||||
@ -276,6 +343,21 @@ files = [
|
||||
{file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.1"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
|
||||
{file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "6.0.0"
|
||||
@ -405,6 +487,18 @@ files = [
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.4.4"
|
||||
@ -742,6 +836,22 @@ files = [
|
||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.0.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2"
|
||||
version = "2.9.6"
|
||||
@ -804,6 +914,66 @@ files = [
|
||||
[package.extras]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.3.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.3.0-py3-none-any.whl", hash = "sha256:933051fa1bfbd38a21e73c3960cebdad4cf59483ddba7696c48509727e17f201"},
|
||||
{file = "pytest-7.3.0.tar.gz", hash = "sha256:58ecc27ebf0ea643ebfdf7fb1249335da761a00c9f955bcd922349bcb68ee57d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "4.0.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
|
||||
{file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coverage = {version = ">=5.2.1", extras = ["toml"]}
|
||||
pytest = ">=4.6"
|
||||
|
||||
[package.extras]
|
||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
version = "3.10.0"
|
||||
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"},
|
||||
{file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=5.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
@ -1040,4 +1210,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "c1993dc151cd67977bd1d330fcef95fa6ea8fc8aa20f241b804bfec61d7a3a9f"
|
||||
content-hash = "9d3d37a64fddc6654db1f2606001b81a2432e8f4f667f7146921942f59472614"
|
||||
|
||||
@ -22,7 +22,13 @@ sqlalchemy = "^2.0.9"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^23.3.0"
|
||||
flake8 = "^6.0.0"
|
||||
pytest = "^7.3.0"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-mock = "^3.10.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[pytest]
|
||||
mock_use_standalone_module = true
|
||||
|
||||
82
tests/mocks/nordigen.py
Normal file
82
tests/mocks/nordigen.py
Normal file
@ -0,0 +1,82 @@
|
||||
id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
|
||||
|
||||
accounts_id = {
|
||||
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"created": "2023-04-13T21:45:59.957Z",
|
||||
"last_accessed": "2023-04-13T21:45:59.957Z",
|
||||
"iban": "string",
|
||||
"institution_id": "string",
|
||||
"status": "DISCOVERED",
|
||||
"owner_name": "string",
|
||||
}
|
||||
|
||||
accounts_id_transactions = {
|
||||
"transactions": {
|
||||
"booked": [
|
||||
{
|
||||
"transactionId": "string",
|
||||
"debtorName": "string",
|
||||
"debtorAccount": {"iban": "string"},
|
||||
"transactionAmount": {"currency": "string", "amount": "328.18"},
|
||||
"bankTransactionCode": "string",
|
||||
"bookingDate": "2023-01-14",
|
||||
"valueDate": "2023-01-15",
|
||||
"remittanceInformationUnstructured": "string",
|
||||
},
|
||||
{
|
||||
"transactionId": "string",
|
||||
"transactionAmount": {"currency": "string", "amount": "947.26"},
|
||||
"bankTransactionCode": "string",
|
||||
"bookingDate": "2023-02-14",
|
||||
"valueDate": "2023-02-15",
|
||||
"remittanceInformationUnstructured": "string",
|
||||
},
|
||||
],
|
||||
"pending": [
|
||||
{
|
||||
"transactionAmount": {"currency": "string", "amount": "float"},
|
||||
"valueDate": "2023-04-14",
|
||||
"remittanceInformationUnstructured": "string",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
requisitions = {
|
||||
"count": 123,
|
||||
"next": "https://ob.nordigen.com/api/v2/requisitions/?limit=100&offset=0",
|
||||
"previous": "https://ob.nordigen.com/api/v2/requisitions/?limit=100&offset=0",
|
||||
"results": [
|
||||
{
|
||||
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"created": "2023-04-13T21:43:45.027Z",
|
||||
"redirect": "string",
|
||||
"status": "CR",
|
||||
"institution_id": "string",
|
||||
"agreement": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"reference": "string",
|
||||
"accounts": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
|
||||
"user_language": "strin",
|
||||
"link": "https://ob.nordigen.com/psd2/start/3fa85f64-5717-4562-b3fc-2c963f66afa6/{$INSTITUTION_ID}",
|
||||
"ssn": "string",
|
||||
"account_selection": False,
|
||||
"redirect_immediate": False,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
requisitions_id = {
|
||||
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"created": "2023-04-13T21:45:12.336Z",
|
||||
"redirect": "string",
|
||||
"status": "CR",
|
||||
"institution_id": "string",
|
||||
"agreement": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
||||
"reference": "string",
|
||||
"accounts": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
|
||||
"user_language": "strin",
|
||||
"link": "https://ob.nordigen.com/psd2/start/3fa85f64-5717-4562-b3fc-2c963f66afa6/{$INSTITUTION_ID}",
|
||||
"ssn": "string",
|
||||
"account_selection": False,
|
||||
"redirect_immediate": False,
|
||||
}
|
||||
94
tests/test_psd2.py
Normal file
94
tests/test_psd2.py
Normal file
@ -0,0 +1,94 @@
|
||||
import datetime as dt
|
||||
from decimal import Decimal
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
import mocks.nordigen as mock
|
||||
|
||||
from pfbudget.db.model import Bank, BankTransaction, Nordigen
|
||||
from pfbudget.extract.credentials import Credentials
|
||||
from pfbudget.extract.exceptions import BankError, CredentialsError
|
||||
from pfbudget.extract.psd2 import PSD2Client
|
||||
|
||||
|
||||
class MockGet:
|
||||
def __init__(self, status_code: int = 200, exception=None):
|
||||
self._ok = True if status_code == 200 else False
|
||||
self._status_code = status_code
|
||||
self._exception = exception
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if self._exception:
|
||||
raise self._exception
|
||||
|
||||
self.url = kwargs["url"]
|
||||
return self
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
return self._ok
|
||||
|
||||
@property
|
||||
def status_code(self):
|
||||
return self._status_code
|
||||
|
||||
def json(self):
|
||||
if self.url.endswith("accounts/" + mock.id + "/"):
|
||||
return mock.accounts_id
|
||||
elif self.url.endswith("accounts/" + mock.id + "/transactions/"):
|
||||
return mock.accounts_id_transactions
|
||||
elif self.url.endswith("requisitions/"):
|
||||
return mock.requisitions
|
||||
elif self.url.endswith("requisitions/" + mock.id + "/"):
|
||||
return mock.requisitions_id
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_requests(monkeypatch):
|
||||
monkeypatch.setattr("requests.get", MockGet())
|
||||
monkeypatch.delattr("requests.post")
|
||||
monkeypatch.delattr("requests.put")
|
||||
monkeypatch.delattr("requests.delete")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> PSD2Client:
|
||||
credentials = Credentials("ID", "KEY", "TOKEN")
|
||||
return PSD2Client(credentials)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def banks() -> list[Bank]:
|
||||
bank = Bank("Bank#1", "", "")
|
||||
bank.nordigen = Nordigen("", "", mock.id, False)
|
||||
return [bank]
|
||||
|
||||
|
||||
class TestExtractPSD2:
|
||||
def test_empty_credentials(self):
|
||||
cred = Credentials("", "")
|
||||
with pytest.raises(CredentialsError):
|
||||
PSD2Client(cred)
|
||||
|
||||
def test_empty_banks(self, client):
|
||||
with pytest.raises(BankError):
|
||||
client.extract([])
|
||||
|
||||
def test_no_psd2_bank(self, client):
|
||||
with pytest.raises(BankError):
|
||||
client.extract([Bank("", "", "")])
|
||||
|
||||
def test_timeout(self, monkeypatch, client, banks):
|
||||
monkeypatch.setattr("requests.get", MockGet(exception=requests.ReadTimeout))
|
||||
with pytest.raises(requests.Timeout):
|
||||
client.extract(banks)
|
||||
|
||||
def test_extract(self, client, banks):
|
||||
assert client.extract(banks) == [
|
||||
BankTransaction(
|
||||
dt.date(2023, 1, 14), "string", Decimal("328.18"), "Bank#1"
|
||||
),
|
||||
BankTransaction(
|
||||
dt.date(2023, 2, 14), "string", Decimal("947.26"), "Bank#1"
|
||||
),
|
||||
]
|
||||
Loading…
x
Reference in New Issue
Block a user