From 60c7028f0b1df97a6c77a18fe9259b68f57d689e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Mon, 22 Jan 2024 21:27:15 +0000 Subject: [PATCH] Expand serializer to more types and test --- pfbudget/core/command.py | 4 +- pfbudget/utils/serializer.py | 43 +++++++++++++++++--- tests/mocks/banks.py | 4 ++ tests/mocks/nordigen.py | 5 +++ tests/mocks/transactions.py | 33 +++++++++++++++ tests/test_command.py | 78 ++++++++++++++++++++++++++++++++++++ tests/test_psd2.py | 7 +++- 7 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 tests/mocks/banks.py create mode 100644 tests/mocks/transactions.py create mode 100644 tests/test_command.py diff --git a/pfbudget/core/command.py b/pfbudget/core/command.py index f1c4903..ae9e1c6 100644 --- a/pfbudget/core/command.py +++ b/pfbudget/core/command.py @@ -30,7 +30,7 @@ class ExportCommand(Command): match self.format: case ExportFormat.JSON: with open(self.fn, "w", newline="") as f: - json.dump([serialize(e) for e in values], f, indent=4, default=str) + json.dump([serialize(e) for e in values], f, indent=4) case ExportFormat.pickle: with open(self.fn, "wb") as f: - pickle.dump([serialize(e) for e in values], f) + pickle.dump(values, f) diff --git a/pfbudget/utils/serializer.py b/pfbudget/utils/serializer.py index 6cb5267..adef7d4 100644 --- a/pfbudget/utils/serializer.py +++ b/pfbudget/utils/serializer.py @@ -3,19 +3,52 @@ from dataclasses import fields from functools import singledispatch from typing import Any -from pfbudget.db.model import Transaction, TransactionCategory +from pfbudget.db.model import Transaction, TransactionCategory, TransactionTag, Note + + +class NotSerializableError(Exception): + pass @singledispatch def serialize(obj: Any) -> Mapping[str, Any]: - raise NotImplementedError + return {field.name: getattr(obj, field.name) for field in fields(obj)} @serialize.register def _(obj: Transaction) -> Mapping[str, Any]: - return dict((field.name, getattr(obj, field.name)) for field in fields(obj)) + category = None + if obj.category: + category = { + "name": obj.category.name, + "selector": str(obj.category.selector.selector) + if obj.category.selector + else None, + } + + return dict( + id=obj.id, + date=obj.date.isoformat(), + description=obj.description, + amount=str(obj.amount), + split=obj.split, + category=category if category else None, + tags=[{"tag": tag.tag} for tag in obj.tags], + note=obj.note, + type=obj.type, + ) @serialize.register -def _(obj: TransactionCategory) -> Mapping[str, Any]: - return dict((field.name, getattr(obj, field.name)) for field in fields(obj)) +def _(_: TransactionCategory) -> Mapping[str, Any]: + raise NotSerializableError("TransactionCategory") + + +@serialize.register +def _(_: TransactionTag) -> Mapping[str, Any]: + raise NotSerializableError("TransactionTag") + + +@serialize.register +def _(_: Note) -> Mapping[str, Any]: + raise NotSerializableError("Note") diff --git a/tests/mocks/banks.py b/tests/mocks/banks.py new file mode 100644 index 0000000..e08d3bc --- /dev/null +++ b/tests/mocks/banks.py @@ -0,0 +1,4 @@ +from pfbudget.db.model import AccountType, Bank + + +checking = Bank("bank", "BANK", AccountType.checking) diff --git a/tests/mocks/nordigen.py b/tests/mocks/nordigen.py index d0a284e..8ef8ee1 100644 --- a/tests/mocks/nordigen.py +++ b/tests/mocks/nordigen.py @@ -1,3 +1,6 @@ +from pfbudget.extract.nordigen import NordigenCredentials + + id = "3fa85f64-5717-4562-b3fc-2c963f66afa6" accounts_id = { @@ -80,3 +83,5 @@ requisitions_id = { "account_selection": False, "redirect_immediate": False, } + +credentials = NordigenCredentials("ID", "KEY", "TOKEN") diff --git a/tests/mocks/transactions.py b/tests/mocks/transactions.py new file mode 100644 index 0000000..c0f4fa9 --- /dev/null +++ b/tests/mocks/transactions.py @@ -0,0 +1,33 @@ +from datetime import date +from decimal import Decimal + +from pfbudget.db.model import ( + CategorySelector, + Selector_T, + Transaction, + TransactionCategory, +) + +simple = [ + Transaction(date(2023, 1, 1), "", Decimal("-10")), + Transaction(date(2023, 1, 2), "", Decimal("-50")), +] + +simple_transformed = [ + Transaction( + date(2023, 1, 1), + "", + Decimal("-10"), + category=TransactionCategory( + "category#1", CategorySelector(Selector_T.algorithm) + ), + ), + Transaction( + date(2023, 1, 2), + "", + Decimal("-50"), + category=TransactionCategory( + "category#2", CategorySelector(Selector_T.algorithm) + ), + ), +] diff --git a/tests/test_command.py b/tests/test_command.py new file mode 100644 index 0000000..f55318c --- /dev/null +++ b/tests/test_command.py @@ -0,0 +1,78 @@ +from collections.abc import Sequence +import json +from pathlib import Path +import pickle +from typing import Any + +import mocks.transactions + +from pfbudget.common.types import ExportFormat +from pfbudget.core.command import ExportCommand +from pfbudget.db.client import Client +from pfbudget.db.model import Transaction +from pfbudget.utils.serializer import serialize + + +class FakeClient(Client): + def __init__(self): + self._transactions = mocks.transactions.simple + + def select(self, what: Any, *_) -> Sequence[Any]: + if what == Transaction: + return self.transactions + return [] + + def insert(self, *_): + pass + + def update(self, *_): + pass + + def delete(self, *_): + pass + + @property + def transactions(self): + return self._transactions + + @transactions.setter + def transactions(self, value: Sequence[Transaction]): + self._transactions = value + + +class TestCommand: + def test_export_json(self, tmp_path: Path): + client = FakeClient() + file = tmp_path / "test.json" + command = ExportCommand(client, Transaction, file, ExportFormat.JSON) + command.execute() + + with open(file, newline="") as f: + result = json.load(f) + assert result == [serialize(t) for t in mocks.transactions.simple] + + client.transactions = mocks.transactions.simple_transformed + command.execute() + + with open(file, newline="") as f: + result = json.load(f) + assert result == [ + serialize(t) for t in mocks.transactions.simple_transformed + ] + + def test_export_pickle(self, tmp_path: Path): + client = FakeClient() + file = tmp_path / "test.pickle" + command = ExportCommand(client, Transaction, file, ExportFormat.pickle) + command.execute() + + with open(file, "rb") as f: + result = pickle.load(f) + assert result == mocks.transactions.simple + + client.transactions = mocks.transactions.simple_transformed + command.execute() + + with open(file, "rb") as f: + result = pickle.load(f) + assert result == mocks.transactions.simple_transformed diff --git a/tests/test_psd2.py b/tests/test_psd2.py index e4f7cf6..a262cdd 100644 --- a/tests/test_psd2.py +++ b/tests/test_psd2.py @@ -88,7 +88,12 @@ class TestExtractPSD2: with pytest.raises(requests.Timeout): extractor.extract(bank) - def test_extract(self, extractor: Extractor, bank: Bank): + def test_extract( + self, monkeypatch: pytest.MonkeyPatch, extractor: Extractor, bank: Bank + ): + monkeypatch.setattr( + "pfbudget.extract.nordigen.NordigenClient.dump", lambda *args: None + ) assert extractor.extract(bank) == [ BankTransaction( dt.date(2023, 1, 14), "string", Decimal("328.18"), bank="Bank#1"