diff --git a/pfbudget/core/manager.py b/pfbudget/core/manager.py index d1b799d..b04e410 100644 --- a/pfbudget/core/manager.py +++ b/pfbudget/core/manager.py @@ -1,47 +1,51 @@ from pfbudget.input.input import Input from pfbudget.input.parsers import parse_data -from pfbudget.common.types import Bank, Banks, Transaction, Transactions -from pfbudget.db.sqlite import DatabaseClient +from pfbudget.db.client import DbClient from pfbudget.utils import convert class Manager: - def __init__(self, db: str): - self.__db = db + def __init__(self, url: str): + self._db = DbClient(url) - def init(self): - client = DatabaseClient(self.__db) - client.init() + # def init(self): + # client = DatabaseClient(self.__db) + # client.init() - def register(self, args: dict): - bank = Bank(args["bank"][0], "", args["requisition"][0], args["invert"]) - client = DatabaseClient(self.__db) - client.register_bank(convert(bank)) + # def register(self, args: dict): + # bank = Bank(args["bank"][0], "", args["requisition"][0], args["invert"]) + # client = DatabaseClient(self.__db) + # client.register_bank(convert(bank)) - def unregister(self, args: dict): - client = DatabaseClient(self.__db) - client.unregister_bank(args["bank"][0]) + # def unregister(self, args: dict): + # client = DatabaseClient(self.__db) + # client.unregister_bank(args["bank"][0]) def parser(self, parser: Input): transactions = parser.parse() - self.add_transactions(transactions) + print(transactions) + # self.add_transactions(transactions) - def parse(self, filename: str, args: dict): - transactions = parse_data(filename, args) - self.add_transactions(transactions) + # def parse(self, filename: str, args: dict): + # transactions = parse_data(filename, args) + # self.add_transactions(transactions) - def transactions() -> list[Transaction]: - pass + # def transactions() -> list[Transaction]: + # pass - def add_transactions(self, transactions: Transactions): - client = DatabaseClient(self.__db) - client.insert_transactions([convert(t) for t in transactions]) + def add_transactions(self, transactions): + with self.db.session() as session: + session.add(transactions) + session.commit() - def get_bank_by(self, key: str, value: str) -> Bank: - client = DatabaseClient(self.__db) - bank = client.get_bank(key, value) - return convert(bank) + # def get_bank_by(self, key: str, value: str) -> Bank: + # client = DatabaseClient(self.__db) + # bank = client.get_bank(key, value) + # return convert(bank) - def get_banks(self) -> Banks: - client = DatabaseClient(self.__db) - return [convert(bank) for bank in client.get_banks()] + def get_banks(self): + return self.db.get_nordigen_banks() + + @property + def db(self): + return self._db diff --git a/pfbudget/db/client.py b/pfbudget/db/client.py new file mode 100644 index 0000000..3da414c --- /dev/null +++ b/pfbudget/db/client.py @@ -0,0 +1,57 @@ +from sqlalchemy import create_engine, select +from sqlalchemy.orm import Session, joinedload, selectinload + +from pfbudget.db.model import Bank, Transaction + +# import logging + +# logging.basicConfig() +# logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) + + +class DbClient: + """ + General database client using sqlalchemy + """ + + __sessions: list[Session] + + def __init__(self, url: str) -> None: + self._engine = create_engine(url) + + def get_transactions(self): + """¿Non-optimized? get_transactions, will load the entire Transaction""" + with Session(self.engine) as session: + stmt = select(Transaction).options( + joinedload("*"), selectinload(Transaction.tags) + ) + return session.scalars(stmt).all() + + def get_uncategorized(self): + with Session(self.engine) as session: + stmt = select(Transaction).where(~Transaction.category.has()) + return session.scalars(stmt).all() + + def get_categorized(self): + with Session(self.engine) as session: + stmt = select(Transaction).where(Transaction.category.has()) + return session.scalars(stmt).all() + + def insert_transactions(self, input: list[Transaction]): + with Session(self.engine) as session: + session.add_all(input) + session.commit() + + def get_banks(self): + with Session(self.engine) as session: + stmt = select(Bank) + return session.scalars(stmt).all() + + def get_nordigen_banks(self): + with Session(self.engine) as session: + stmt = select(Bank).where(Bank.nordigen.has()) + return session.scalars(stmt).all() + + @property + def engine(self): + return self._engine diff --git a/pfbudget/db/model.py b/pfbudget/db/model.py index 385dabd..16837c5 100644 --- a/pfbudget/db/model.py +++ b/pfbudget/db/model.py @@ -9,13 +9,7 @@ from sqlalchemy import ( String, Text, ) -from sqlalchemy.orm import ( - DeclarativeBase, - Mapped, - mapped_column, - MappedAsDataclass, - relationship, -) +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship from decimal import Decimal from typing import Annotated, Optional @@ -23,7 +17,7 @@ import datetime as dt import enum -class Base(MappedAsDataclass, DeclarativeBase): +class Base(DeclarativeBase): __table_args__ = {"schema": "transactions"} metadata = MetaData( naming_convention={ @@ -58,6 +52,13 @@ class Bank(Base): BIC: Mapped[str] = mapped_column(String(8), primary_key=True) type: Mapped[accounttype] = mapped_column(primary_key=True) + nordigen: Mapped[Optional[Nordigen]] = relationship( + back_populates="bank", lazy="joined" + ) + + def __repr__(self) -> str: + return f"Bank(name={self.name}, BIC={self.BIC}, type={self.type}, nordigen={self.nordigen})" + bankfk = Annotated[str, mapped_column(Text, ForeignKey(Bank.name))] @@ -65,7 +66,7 @@ idpk = Annotated[int, mapped_column(BigInteger, primary_key=True)] money = Annotated[Decimal, mapped_column(Numeric(16, 2), nullable=False)] -class Original(Base): +class Transaction(Base): __tablename__ = "originals" id: Mapped[idpk] = mapped_column(autoincrement=True) @@ -74,15 +75,20 @@ class Original(Base): bank: Mapped[bankfk] amount: Mapped[money] - category: Mapped[Category] = relationship(back_populates="original") - note: Mapped[Note] = relationship(back_populates="original") - tags: Mapped[set[Tag]] = relationship( + category: Mapped[Optional[Category]] = relationship( + back_populates="original", lazy="joined" + ) + note: Mapped[Optional[Note]] = relationship(back_populates="original") + tags: Mapped[Optional[set[Tag]]] = relationship( back_populates="original", cascade="all, delete-orphan", passive_deletes=True ) + def __repr__(self) -> str: + return f"Transaction(date={self.date}, description={self.description}, bank={self.bank}, amount={self.amount}, category={self.category})" + idfk = Annotated[ - int, mapped_column(BigInteger, ForeignKey(Original.id, ondelete="CASCADE")) + int, mapped_column(BigInteger, ForeignKey(Transaction.id, ondelete="CASCADE")) ] @@ -92,7 +98,10 @@ class Category(Base): id: Mapped[idfk] = mapped_column(primary_key=True) category: Mapped[str] - original: Mapped[Original] = relationship(back_populates="category") + original: Mapped[Transaction] = relationship(back_populates="category") + + def __repr__(self) -> str: + return f"Category({self.category})" class Note(Base): @@ -101,7 +110,7 @@ class Note(Base): id: Mapped[idfk] = mapped_column(primary_key=True) note: Mapped[str] - original: Mapped[Original] = relationship(back_populates="note") + original: Mapped[Transaction] = relationship(back_populates="note") class Nordigen(Base): @@ -112,6 +121,11 @@ class Nordigen(Base): requisition_id: Mapped[Optional[str]] invert: Mapped[Optional[bool]] + bank: Mapped[Bank] = relationship(back_populates="nordigen") + + def __repr__(self) -> str: + return f"(bank_id={self.bank_id}, requisition_id={self.requisition_id}, invert={self.invert})" + class Tag(Base): __tablename__ = "tags" @@ -119,4 +133,4 @@ class Tag(Base): id: Mapped[idfk] = mapped_column(primary_key=True) tag: Mapped[str] = mapped_column(primary_key=True) - original: Mapped[Original] = relationship(back_populates="tags") + original: Mapped[Transaction] = relationship(back_populates="tags") diff --git a/pfbudget/input/nordigen.py b/pfbudget/input/nordigen.py index fa1a128..e43f878 100644 --- a/pfbudget/input/nordigen.py +++ b/pfbudget/input/nordigen.py @@ -9,7 +9,8 @@ import os import webbrowser from .input import Input -from pfbudget.common.types import NoBankSelected, Transactions +from pfbudget.common.types import NoBankSelected +from pfbudget.db.model import Transaction from pfbudget.utils import convert load_dotenv() @@ -43,7 +44,7 @@ class NordigenInput(Input): self.__from = start self.__to = end - def parse(self) -> Transactions: + def parse(self) -> list[Transaction]: transactions = [] if not self.__banks: raise NoBankSelected @@ -51,7 +52,7 @@ class NordigenInput(Input): for bank in self.__banks: print(f"Downloading from {bank}...") requisition = self.client.requisition.get_requisition_by_id( - bank.requisition_id + bank.nordigen.requisition_id ) for acc in requisition["accounts"]: diff --git a/pfbudget/utils/converters.py b/pfbudget/utils/converters.py index b3f27df..287ea1b 100644 --- a/pfbudget/utils/converters.py +++ b/pfbudget/utils/converters.py @@ -1,7 +1,8 @@ -from datetime import timedelta +from datetime import date, timedelta from functools import singledispatch -from pfbudget.common.types import Bank, Transaction, TransactionError +from pfbudget.common.types import TransactionError +from pfbudget.db.model import Bank, Transaction from pfbudget.db.schema import DbBank, DbTransaction from .utils import parse_decimal @@ -12,60 +13,60 @@ def convert(t): pass -@convert.register -def _(t: Transaction) -> DbTransaction: - return DbTransaction( - t.date, - t.description, - t.bank, - t.value, - t.category, - t.original, - t.additional_comment, - ) +# @convert.register +# def _(t: Transaction) -> DbTransaction: +# return DbTransaction( +# t.date, +# t.description, +# t.bank, +# t.value, +# t.category, +# t.original, +# t.additional_comment, +# ) -@convert.register -def _(db: DbTransaction) -> Transaction: - try: - return Transaction(db) - except TransactionError: - print(f"{db} is in the wrong format") +# @convert.register +# def _(db: DbTransaction) -> Transaction: +# try: +# return Transaction(db) +# except TransactionError: +# print(f"{db} is in the wrong format") -@convert.register -def _(db: DbBank, key: str = "") -> Bank: - bank = Bank(db.name, db.bic, db.requisition_id, db.invert, db.offset, key=key) - if not bank.invert: - bank.invert = False - if not bank.offset: - bank.offset = 0 - return bank +# @convert.register +# def _(db: DbBank, key: str = "") -> Bank: +# bank = Bank(db.name, db.bic, db.requisition_id, db.invert, db.offset, key=key) +# if not bank.invert: +# bank.invert = False +# if not bank.offset: +# bank.offset = 0 +# return bank -@convert.register -def _(bank: Bank) -> DbBank: - bank = DbBank( - bank.name, bank.bic, "", "", bank.requisition_id, bank.invert, bank.offset - ) - if not bank.invert: - bank.invert = False - if not bank.offset: - bank.offset = 0 - return bank +# @convert.register +# def _(bank: Bank) -> DbBank: +# bank = DbBank( +# bank.name, bank.bic, "", "", bank.requisition_id, bank.invert, bank.offset +# ) +# if not bank.invert: +# bank.invert = False +# if not bank.offset: +# bank.offset = 0 +# return bank @convert.register def _(json: dict, bank: Bank) -> Transaction: - i = -1 if bank.invert else 1 + i = -1 if bank.nordigen.invert else 1 try: transaction = Transaction( - json["bookingDate"], - json["remittanceInformationUnstructured"], - bank.name, - i * parse_decimal(json["transactionAmount"]["amount"]), + date=date.fromisoformat(json["bookingDate"]), + description=json["remittanceInformationUnstructured"], + bank=bank.name, + amount=i * parse_decimal(json["transactionAmount"]["amount"]), ) - transaction.date += timedelta(days=bank.offset) + # transaction.date += timedelta(days=bank.offset) return transaction except TransactionError: