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 csv
|
||||||
|
import dotenv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pickle
|
import pickle
|
||||||
|
import os
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
from pfbudget.common.types import Operation
|
from pfbudget.common.types import Operation
|
||||||
@ -25,9 +27,13 @@ from pfbudget.db.model import (
|
|||||||
Transaction,
|
Transaction,
|
||||||
TransactionCategory,
|
TransactionCategory,
|
||||||
)
|
)
|
||||||
|
from pfbudget.extract.credentials import Credentials
|
||||||
|
from pfbudget.extract.extract import Extract
|
||||||
from pfbudget.extract.psd2 import PSD2Client
|
from pfbudget.extract.psd2 import PSD2Client
|
||||||
from pfbudget.extract.parsers import parse_data
|
from pfbudget.extract.parsers import parse_data
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
class Manager:
|
class Manager:
|
||||||
def __init__(self, db: str, verbosity: int = 0):
|
def __init__(self, db: str, verbosity: int = 0):
|
||||||
@ -72,16 +78,16 @@ class Manager:
|
|||||||
session.add(sorted(transactions))
|
session.add(sorted(transactions))
|
||||||
|
|
||||||
case Operation.Download:
|
case Operation.Download:
|
||||||
client = PSD2Client()
|
client = Manager.nordigen_client()
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
if len(params[3]) == 0:
|
if len(params[3]) == 0:
|
||||||
client.banks = session.get(Bank, Bank.nordigen)
|
banks = session.get(Bank, Bank.nordigen)
|
||||||
else:
|
else:
|
||||||
client.banks = session.get(Bank, Bank.name, params[3])
|
banks = session.get(Bank, Bank.name, params[3])
|
||||||
session.expunge_all()
|
session.expunge_all()
|
||||||
client.start = params[0]
|
client.start = params[0]
|
||||||
client.end = params[1]
|
client.end = params[1]
|
||||||
transactions = client.parse()
|
transactions = client.extract(banks)
|
||||||
|
|
||||||
# dry-run
|
# dry-run
|
||||||
if not params[2]:
|
if not params[2]:
|
||||||
@ -116,15 +122,15 @@ class Manager:
|
|||||||
session.remove_by_name(Nordigen, params)
|
session.remove_by_name(Nordigen, params)
|
||||||
|
|
||||||
case Operation.Token:
|
case Operation.Token:
|
||||||
PSD2Client().token()
|
Manager.nordigen_client().generate_token()
|
||||||
|
|
||||||
case Operation.RequisitionId:
|
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]}")
|
print(f"Opening {link} to request access to {params[0]}")
|
||||||
webbrowser.open(link)
|
webbrowser.open(link)
|
||||||
|
|
||||||
case Operation.PSD2CountryBanks:
|
case Operation.PSD2CountryBanks:
|
||||||
banks = PSD2Client().country_banks(params[0])
|
banks = Manager.nordigen_client().country_banks(params[0])
|
||||||
print(banks)
|
print(banks)
|
||||||
|
|
||||||
case (
|
case (
|
||||||
@ -411,3 +417,12 @@ class Manager:
|
|||||||
@db.setter
|
@db.setter
|
||||||
def db(self, url: str):
|
def db(self, url: str):
|
||||||
self._db = url
|
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):
|
class Extract(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse(self) -> list[Transaction]:
|
def extract(self) -> list[Transaction]:
|
||||||
return NotImplementedError
|
return NotImplementedError
|
||||||
|
|||||||
@ -1,75 +1,55 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
import dotenv
|
|
||||||
import json
|
import json
|
||||||
import nordigen
|
import nordigen
|
||||||
import os
|
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
import pfbudget.db.model as t
|
import pfbudget.db.model as t
|
||||||
from pfbudget.utils.converters import convert
|
from pfbudget.utils.converters import convert
|
||||||
|
|
||||||
|
from .credentials import Credentials
|
||||||
|
from .exceptions import BankError, CredentialsError
|
||||||
from .extract import Extract
|
from .extract import Extract
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
|
||||||
|
|
||||||
|
|
||||||
class PSD2Client(Extract):
|
class PSD2Client(Extract):
|
||||||
redirect_url = "https://murta.dev"
|
redirect_url = "https://murta.dev"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, credentials: Credentials):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
if not (key := os.environ.get("SECRET_KEY")) or not (
|
if not credentials.valid():
|
||||||
id := os.environ.get("SECRET_ID")
|
raise CredentialsError
|
||||||
):
|
|
||||||
raise
|
|
||||||
|
|
||||||
self._client = nordigen.NordigenClient(
|
self._client = nordigen.NordigenClient(
|
||||||
secret_key=key,
|
secret_key=credentials.key, secret_id=credentials.id, timeout=5
|
||||||
secret_id=id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._client.token = self.__token()
|
if credentials.token:
|
||||||
|
self._token = credentials.token
|
||||||
|
|
||||||
self._start = dt.date.min
|
self._start = dt.date.min
|
||||||
self._end = dt.date.max
|
self._end = dt.date.max
|
||||||
|
|
||||||
def parse(self) -> list[t.BankTransaction]:
|
def extract(self, banks: Sequence[t.Bank]) -> list[t.BankTransaction]:
|
||||||
transactions = []
|
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:
|
for bank in banks:
|
||||||
print(f"Downloading from {bank}...")
|
downloaded = None
|
||||||
requisition = self.client.requisition.get_requisition_by_id(
|
|
||||||
bank.nordigen.requisition_id
|
|
||||||
)
|
|
||||||
|
|
||||||
print(requisition)
|
|
||||||
for acc in requisition["accounts"]:
|
|
||||||
account = self._client.account_api(acc)
|
|
||||||
|
|
||||||
retries = 0
|
|
||||||
downloaded = {}
|
|
||||||
while retries < 3:
|
|
||||||
try:
|
try:
|
||||||
downloaded = account.get_transactions()
|
print(f"Downloading from {bank}...")
|
||||||
break
|
downloaded = self.download(bank.nordigen.requisition_id)
|
||||||
except requests.ReadTimeout:
|
|
||||||
retries += 1
|
|
||||||
print(f"Request #{retries} timed-out, retrying in 1s")
|
|
||||||
time.sleep(1)
|
|
||||||
except requests.HTTPError as e:
|
except requests.HTTPError as e:
|
||||||
retries += 1
|
print(f"There was an issue downloading from {bank.name} -> {e}")
|
||||||
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
|
continue
|
||||||
|
|
||||||
with open("json/" + bank.name + ".json", "w") as f:
|
if downloaded:
|
||||||
json.dump(downloaded, f)
|
self.dump(bank, downloaded)
|
||||||
|
|
||||||
converted = [
|
converted = [
|
||||||
convert(t, bank) for t in downloaded["transactions"]["booked"]
|
convert(t, bank) for t in downloaded["transactions"]["booked"]
|
||||||
@ -81,10 +61,40 @@ class PSD2Client(Extract):
|
|||||||
|
|
||||||
return sorted(transactions)
|
return sorted(transactions)
|
||||||
|
|
||||||
def token(self):
|
def download(self, requisition_id):
|
||||||
token = self._client.generate_token()
|
requisition = self._client.requisition.get_requisition_by_id(requisition_id)
|
||||||
print(f"New access token: {token}")
|
print(requisition)
|
||||||
return token
|
|
||||||
|
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"):
|
def requisition(self, id: str, country: str = "PT"):
|
||||||
requisition = self._client.initialize_session(
|
requisition = self._client.initialize_session(
|
||||||
@ -97,18 +107,6 @@ class PSD2Client(Extract):
|
|||||||
def country_banks(self, country: str):
|
def country_banks(self, country: str):
|
||||||
return self._client.institution.get_institutions(country)
|
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
|
@property
|
||||||
def start(self):
|
def start(self):
|
||||||
return self._start
|
return self._start
|
||||||
@ -125,10 +123,20 @@ class PSD2Client(Extract):
|
|||||||
def end(self, value):
|
def end(self, value):
|
||||||
self._end = value
|
self._end = value
|
||||||
|
|
||||||
def __token(self):
|
# def __token(self):
|
||||||
if token := os.environ.get("TOKEN"):
|
# if token := os.environ.get("TOKEN"):
|
||||||
return token
|
# return token
|
||||||
else:
|
# else:
|
||||||
token = self._client.generate_token()
|
# token = self._client.generate_token()
|
||||||
print(f"New access token: {token}")
|
# print(f"New access token: {token}")
|
||||||
return token["access"]
|
# 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 = ["Pillow", "matplotlib", "pytest"]
|
||||||
test-no-images = ["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]]
|
[[package]]
|
||||||
name = "cycler"
|
name = "cycler"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -276,6 +343,21 @@ files = [
|
|||||||
{file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
|
{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]]
|
[[package]]
|
||||||
name = "flake8"
|
name = "flake8"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@ -405,6 +487,18 @@ files = [
|
|||||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
{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]]
|
[[package]]
|
||||||
name = "kiwisolver"
|
name = "kiwisolver"
|
||||||
version = "1.4.4"
|
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)"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "psycopg2"
|
name = "psycopg2"
|
||||||
version = "2.9.6"
|
version = "2.9.6"
|
||||||
@ -804,6 +914,66 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
diagrams = ["jinja2", "railroad-diagrams"]
|
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]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
@ -1040,4 +1210,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "c1993dc151cd67977bd1d330fcef95fa6ea8fc8aa20f241b804bfec61d7a3a9f"
|
content-hash = "9d3d37a64fddc6654db1f2606001b81a2432e8f4f667f7146921942f59472614"
|
||||||
|
|||||||
@ -22,7 +22,13 @@ sqlalchemy = "^2.0.9"
|
|||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^23.3.0"
|
black = "^23.3.0"
|
||||||
flake8 = "^6.0.0"
|
flake8 = "^6.0.0"
|
||||||
|
pytest = "^7.3.0"
|
||||||
|
pytest-cov = "^4.0.0"
|
||||||
|
pytest-mock = "^3.10.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
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