Nordigen -> PostgreSQL path completed
Can now download from all banks registered on the banks/nordigen table and write to the PostgresSQL DB (or any DB, since we're now agnostic). Commented out most of the managers functions until the integration with the new DB client is complete. Set Optional relationships. Remove the DB types as dataclasses, it only increased the verbosity of the types w/o much going for it. Change the name Original to Transaction, since the type is the placeholder for the rest of the transaction information.
This commit is contained in:
parent
91514f71b1
commit
e379d77995
@ -1,47 +1,51 @@
|
|||||||
from pfbudget.input.input import Input
|
from pfbudget.input.input import Input
|
||||||
from pfbudget.input.parsers import parse_data
|
from pfbudget.input.parsers import parse_data
|
||||||
from pfbudget.common.types import Bank, Banks, Transaction, Transactions
|
from pfbudget.db.client import DbClient
|
||||||
from pfbudget.db.sqlite import DatabaseClient
|
|
||||||
from pfbudget.utils import convert
|
from pfbudget.utils import convert
|
||||||
|
|
||||||
|
|
||||||
class Manager:
|
class Manager:
|
||||||
def __init__(self, db: str):
|
def __init__(self, url: str):
|
||||||
self.__db = db
|
self._db = DbClient(url)
|
||||||
|
|
||||||
def init(self):
|
# def init(self):
|
||||||
client = DatabaseClient(self.__db)
|
# client = DatabaseClient(self.__db)
|
||||||
client.init()
|
# client.init()
|
||||||
|
|
||||||
def register(self, args: dict):
|
# def register(self, args: dict):
|
||||||
bank = Bank(args["bank"][0], "", args["requisition"][0], args["invert"])
|
# bank = Bank(args["bank"][0], "", args["requisition"][0], args["invert"])
|
||||||
client = DatabaseClient(self.__db)
|
# client = DatabaseClient(self.__db)
|
||||||
client.register_bank(convert(bank))
|
# client.register_bank(convert(bank))
|
||||||
|
|
||||||
def unregister(self, args: dict):
|
# def unregister(self, args: dict):
|
||||||
client = DatabaseClient(self.__db)
|
# client = DatabaseClient(self.__db)
|
||||||
client.unregister_bank(args["bank"][0])
|
# client.unregister_bank(args["bank"][0])
|
||||||
|
|
||||||
def parser(self, parser: Input):
|
def parser(self, parser: Input):
|
||||||
transactions = parser.parse()
|
transactions = parser.parse()
|
||||||
self.add_transactions(transactions)
|
print(transactions)
|
||||||
|
# self.add_transactions(transactions)
|
||||||
|
|
||||||
def parse(self, filename: str, args: dict):
|
# def parse(self, filename: str, args: dict):
|
||||||
transactions = parse_data(filename, args)
|
# transactions = parse_data(filename, args)
|
||||||
self.add_transactions(transactions)
|
# self.add_transactions(transactions)
|
||||||
|
|
||||||
def transactions() -> list[Transaction]:
|
# def transactions() -> list[Transaction]:
|
||||||
pass
|
# pass
|
||||||
|
|
||||||
def add_transactions(self, transactions: Transactions):
|
def add_transactions(self, transactions):
|
||||||
client = DatabaseClient(self.__db)
|
with self.db.session() as session:
|
||||||
client.insert_transactions([convert(t) for t in transactions])
|
session.add(transactions)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
def get_bank_by(self, key: str, value: str) -> Bank:
|
# def get_bank_by(self, key: str, value: str) -> Bank:
|
||||||
client = DatabaseClient(self.__db)
|
# client = DatabaseClient(self.__db)
|
||||||
bank = client.get_bank(key, value)
|
# bank = client.get_bank(key, value)
|
||||||
return convert(bank)
|
# return convert(bank)
|
||||||
|
|
||||||
def get_banks(self) -> Banks:
|
def get_banks(self):
|
||||||
client = DatabaseClient(self.__db)
|
return self.db.get_nordigen_banks()
|
||||||
return [convert(bank) for bank in client.get_banks()]
|
|
||||||
|
@property
|
||||||
|
def db(self):
|
||||||
|
return self._db
|
||||||
|
|||||||
57
pfbudget/db/client.py
Normal file
57
pfbudget/db/client.py
Normal file
@ -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
|
||||||
@ -9,13 +9,7 @@ from sqlalchemy import (
|
|||||||
String,
|
String,
|
||||||
Text,
|
Text,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
DeclarativeBase,
|
|
||||||
Mapped,
|
|
||||||
mapped_column,
|
|
||||||
MappedAsDataclass,
|
|
||||||
relationship,
|
|
||||||
)
|
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated, Optional
|
||||||
@ -23,7 +17,7 @@ import datetime as dt
|
|||||||
import enum
|
import enum
|
||||||
|
|
||||||
|
|
||||||
class Base(MappedAsDataclass, DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
__table_args__ = {"schema": "transactions"}
|
__table_args__ = {"schema": "transactions"}
|
||||||
metadata = MetaData(
|
metadata = MetaData(
|
||||||
naming_convention={
|
naming_convention={
|
||||||
@ -58,6 +52,13 @@ class Bank(Base):
|
|||||||
BIC: Mapped[str] = mapped_column(String(8), primary_key=True)
|
BIC: Mapped[str] = mapped_column(String(8), primary_key=True)
|
||||||
type: Mapped[accounttype] = mapped_column(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))]
|
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)]
|
money = Annotated[Decimal, mapped_column(Numeric(16, 2), nullable=False)]
|
||||||
|
|
||||||
|
|
||||||
class Original(Base):
|
class Transaction(Base):
|
||||||
__tablename__ = "originals"
|
__tablename__ = "originals"
|
||||||
|
|
||||||
id: Mapped[idpk] = mapped_column(autoincrement=True)
|
id: Mapped[idpk] = mapped_column(autoincrement=True)
|
||||||
@ -74,15 +75,20 @@ class Original(Base):
|
|||||||
bank: Mapped[bankfk]
|
bank: Mapped[bankfk]
|
||||||
amount: Mapped[money]
|
amount: Mapped[money]
|
||||||
|
|
||||||
category: Mapped[Category] = relationship(back_populates="original")
|
category: Mapped[Optional[Category]] = relationship(
|
||||||
note: Mapped[Note] = relationship(back_populates="original")
|
back_populates="original", lazy="joined"
|
||||||
tags: Mapped[set[Tag]] = relationship(
|
)
|
||||||
|
note: Mapped[Optional[Note]] = relationship(back_populates="original")
|
||||||
|
tags: Mapped[Optional[set[Tag]]] = relationship(
|
||||||
back_populates="original", cascade="all, delete-orphan", passive_deletes=True
|
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[
|
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)
|
id: Mapped[idfk] = mapped_column(primary_key=True)
|
||||||
category: Mapped[str]
|
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):
|
class Note(Base):
|
||||||
@ -101,7 +110,7 @@ class Note(Base):
|
|||||||
id: Mapped[idfk] = mapped_column(primary_key=True)
|
id: Mapped[idfk] = mapped_column(primary_key=True)
|
||||||
note: Mapped[str]
|
note: Mapped[str]
|
||||||
|
|
||||||
original: Mapped[Original] = relationship(back_populates="note")
|
original: Mapped[Transaction] = relationship(back_populates="note")
|
||||||
|
|
||||||
|
|
||||||
class Nordigen(Base):
|
class Nordigen(Base):
|
||||||
@ -112,6 +121,11 @@ class Nordigen(Base):
|
|||||||
requisition_id: Mapped[Optional[str]]
|
requisition_id: Mapped[Optional[str]]
|
||||||
invert: Mapped[Optional[bool]]
|
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):
|
class Tag(Base):
|
||||||
__tablename__ = "tags"
|
__tablename__ = "tags"
|
||||||
@ -119,4 +133,4 @@ class Tag(Base):
|
|||||||
id: Mapped[idfk] = mapped_column(primary_key=True)
|
id: Mapped[idfk] = mapped_column(primary_key=True)
|
||||||
tag: Mapped[str] = 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")
|
||||||
|
|||||||
@ -9,7 +9,8 @@ import os
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
from .input import Input
|
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
|
from pfbudget.utils import convert
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@ -43,7 +44,7 @@ class NordigenInput(Input):
|
|||||||
self.__from = start
|
self.__from = start
|
||||||
self.__to = end
|
self.__to = end
|
||||||
|
|
||||||
def parse(self) -> Transactions:
|
def parse(self) -> list[Transaction]:
|
||||||
transactions = []
|
transactions = []
|
||||||
if not self.__banks:
|
if not self.__banks:
|
||||||
raise NoBankSelected
|
raise NoBankSelected
|
||||||
@ -51,7 +52,7 @@ class NordigenInput(Input):
|
|||||||
for bank in self.__banks:
|
for bank in self.__banks:
|
||||||
print(f"Downloading from {bank}...")
|
print(f"Downloading from {bank}...")
|
||||||
requisition = self.client.requisition.get_requisition_by_id(
|
requisition = self.client.requisition.get_requisition_by_id(
|
||||||
bank.requisition_id
|
bank.nordigen.requisition_id
|
||||||
)
|
)
|
||||||
|
|
||||||
for acc in requisition["accounts"]:
|
for acc in requisition["accounts"]:
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from datetime import timedelta
|
from datetime import date, timedelta
|
||||||
from functools import singledispatch
|
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 pfbudget.db.schema import DbBank, DbTransaction
|
||||||
from .utils import parse_decimal
|
from .utils import parse_decimal
|
||||||
|
|
||||||
@ -12,60 +13,60 @@ def convert(t):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@convert.register
|
# @convert.register
|
||||||
def _(t: Transaction) -> DbTransaction:
|
# def _(t: Transaction) -> DbTransaction:
|
||||||
return DbTransaction(
|
# return DbTransaction(
|
||||||
t.date,
|
# t.date,
|
||||||
t.description,
|
# t.description,
|
||||||
t.bank,
|
# t.bank,
|
||||||
t.value,
|
# t.value,
|
||||||
t.category,
|
# t.category,
|
||||||
t.original,
|
# t.original,
|
||||||
t.additional_comment,
|
# t.additional_comment,
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
@convert.register
|
# @convert.register
|
||||||
def _(db: DbTransaction) -> Transaction:
|
# def _(db: DbTransaction) -> Transaction:
|
||||||
try:
|
# try:
|
||||||
return Transaction(db)
|
# return Transaction(db)
|
||||||
except TransactionError:
|
# except TransactionError:
|
||||||
print(f"{db} is in the wrong format")
|
# print(f"{db} is in the wrong format")
|
||||||
|
|
||||||
|
|
||||||
@convert.register
|
# @convert.register
|
||||||
def _(db: DbBank, key: str = "") -> Bank:
|
# def _(db: DbBank, key: str = "") -> Bank:
|
||||||
bank = Bank(db.name, db.bic, db.requisition_id, db.invert, db.offset, key=key)
|
# bank = Bank(db.name, db.bic, db.requisition_id, db.invert, db.offset, key=key)
|
||||||
if not bank.invert:
|
# if not bank.invert:
|
||||||
bank.invert = False
|
# bank.invert = False
|
||||||
if not bank.offset:
|
# if not bank.offset:
|
||||||
bank.offset = 0
|
# bank.offset = 0
|
||||||
return bank
|
# return bank
|
||||||
|
|
||||||
|
|
||||||
@convert.register
|
# @convert.register
|
||||||
def _(bank: Bank) -> DbBank:
|
# def _(bank: Bank) -> DbBank:
|
||||||
bank = DbBank(
|
# bank = DbBank(
|
||||||
bank.name, bank.bic, "", "", bank.requisition_id, bank.invert, bank.offset
|
# bank.name, bank.bic, "", "", bank.requisition_id, bank.invert, bank.offset
|
||||||
)
|
# )
|
||||||
if not bank.invert:
|
# if not bank.invert:
|
||||||
bank.invert = False
|
# bank.invert = False
|
||||||
if not bank.offset:
|
# if not bank.offset:
|
||||||
bank.offset = 0
|
# bank.offset = 0
|
||||||
return bank
|
# return bank
|
||||||
|
|
||||||
|
|
||||||
@convert.register
|
@convert.register
|
||||||
def _(json: dict, bank: Bank) -> Transaction:
|
def _(json: dict, bank: Bank) -> Transaction:
|
||||||
i = -1 if bank.invert else 1
|
i = -1 if bank.nordigen.invert else 1
|
||||||
try:
|
try:
|
||||||
transaction = Transaction(
|
transaction = Transaction(
|
||||||
json["bookingDate"],
|
date=date.fromisoformat(json["bookingDate"]),
|
||||||
json["remittanceInformationUnstructured"],
|
description=json["remittanceInformationUnstructured"],
|
||||||
bank.name,
|
bank=bank.name,
|
||||||
i * parse_decimal(json["transactionAmount"]["amount"]),
|
amount=i * parse_decimal(json["transactionAmount"]["amount"]),
|
||||||
)
|
)
|
||||||
transaction.date += timedelta(days=bank.offset)
|
# transaction.date += timedelta(days=bank.offset)
|
||||||
return transaction
|
return transaction
|
||||||
|
|
||||||
except TransactionError:
|
except TransactionError:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user