From 5cea645062c553fc8a87a0e78cbb1ea6added3bf Mon Sep 17 00:00:00 2001 From: Mathias Scheider Date: Fri, 16 Oct 2026 16:27:50 +0200 Subject: [PATCH] Some more refactoring --- src/db_utils.py | 34 ++++++++++++++++++++++++ src/helper/__init__.py | 1 + src/helper/category.py | 22 ++++++++++++++++ src/ingest/b4w.py | 6 +++-- src/ingest/comdirect.py | 4 +-- src/ingest/common.py | 58 ----------------------------------------- src/models/__init__.py | 53 ++++++++++++++++++++++++++++++------- src/models/category.py | 7 +---- tests/test_helper_db.py | 2 +- 9 files changed, 108 insertions(+), 79 deletions(-) create mode 100644 src/db_utils.py create mode 100644 src/helper/__init__.py create mode 100644 src/helper/category.py diff --git a/src/db_utils.py b/src/db_utils.py new file mode 100644 index 0000000..df521bc --- /dev/null +++ b/src/db_utils.py @@ -0,0 +1,34 @@ +from typing import Any + +from sqlalchemy.orm import DeclarativeBase, Session + + +def _query_existing[DbModelT: DeclarativeBase]( + session: Session, DbModel: type[DbModelT], query_parameters: dict[str, Any] +) -> DbModelT | None: + query = session.query(DbModel).filter_by(**query_parameters) + return query.first() + + +def upsert[DbModelT: DeclarativeBase]( + session: Session, + DbModel: type[DbModelT], + parameters_existing: dict[str, Any], + parameters_new: dict[str, Any], + do_update: bool = False, + do_commit: bool = True, +) -> DbModelT: + existing_db_row = _query_existing(session, DbModel, parameters_existing) + + if existing_db_row: + if do_update: + for key, value in parameters_new.items(): + setattr(existing_db_row, key, value) + return existing_db_row + + new_db_row = DbModel(**parameters_new) + session.add(new_db_row) + session.flush() + if do_commit: + session.commit() + return new_db_row diff --git a/src/helper/__init__.py b/src/helper/__init__.py new file mode 100644 index 0000000..b99435e --- /dev/null +++ b/src/helper/__init__.py @@ -0,0 +1 @@ +"""Database helper for application-specific persistence operations.""" diff --git a/src/helper/category.py b/src/helper/category.py new file mode 100644 index 0000000..208b887 --- /dev/null +++ b/src/helper/category.py @@ -0,0 +1,22 @@ +from sqlalchemy.orm import Session + +from src.db_utils import upsert +from src.models.category import Category, CategoryDb + + +def upsert_category( + session: Session, category: Category | None, do_commit: bool = True +) -> CategoryDb | None: + if category is None: + return None + + parent = upsert_category(session, category.parent, do_commit=False) + parameters = {"name": category.name, "parent": parent} + return upsert( + session, + CategoryDb, + parameters, + parameters, + do_update=False, + do_commit=do_commit, + ) diff --git a/src/ingest/b4w.py b/src/ingest/b4w.py index dc9feba..7186c22 100644 --- a/src/ingest/b4w.py +++ b/src/ingest/b4w.py @@ -2,7 +2,9 @@ from pathlib import Path from sqlalchemy.orm import Session -from src.ingest.common import _reader_raw, _upsert, upsert_category +from src.db_utils import upsert +from src.helper.category import upsert_category +from src.ingest.common import _reader_raw from src.ingest.transactions import consolidate_transactions, map_b4w_to_transaction from src.models.b4w import B4wDb, B4wExport @@ -35,7 +37,7 @@ def upsert_b4w( "notes": data.notes, } - return _upsert( + return upsert( session, B4wDb, parameters_existing, diff --git a/src/ingest/comdirect.py b/src/ingest/comdirect.py index 50879a0..197d482 100644 --- a/src/ingest/comdirect.py +++ b/src/ingest/comdirect.py @@ -6,7 +6,7 @@ from typing import cast from sqlalchemy.orm import Session from settings import BANK_ACCOUNTS -from src.ingest.common import _upsert +from src.db_utils import upsert from src.ingest.transactions import ( consolidate_transactions, map_comdirect_giro_to_transaction, @@ -137,7 +137,7 @@ def upsert_comdirect_giro( "text": data.text, } - return _upsert( + return upsert( session, ComdirectGiroDb, parameters_existing, diff --git a/src/ingest/common.py b/src/ingest/common.py index b6318f1..d3e49dd 100644 --- a/src/ingest/common.py +++ b/src/ingest/common.py @@ -1,65 +1,7 @@ import csv from pathlib import Path -from typing import Any - -from sqlalchemy.orm import Session - -from src.models import Category -from src.models.category import CategoryDb -from src.models.common import BaseModelDb - - -def _query_existing( - session: Session, DbModel: type[BaseModelDb], query_parameters: dict[str, Any] -) -> BaseModelDb | None: - query = session.query(DbModel).filter_by(**query_parameters) - return query.first() - - -def _upsert[DbModelT: BaseModelDb]( - session: Session, - DbModel: type[DbModelT], - parameters_existing: dict[str, Any], - parameters_new: dict[str, Any], - do_update: bool = False, - do_commit: bool = True, -) -> DbModelT: - existing_db_row = _query_existing(session, DbModel, parameters_existing) - - if existing_db_row: - if do_update: - for key, value in parameters_new.items(): - setattr(existing_db_row, key, value) - assert isinstance(existing_db_row, DbModel) - return existing_db_row - - new_db_row = DbModel(**parameters_new) - session.add(new_db_row) - session.flush() - if do_commit: - session.commit() - return new_db_row def _reader_raw(path: Path): with path.open() as fid: return list(csv.DictReader(fid)) - - -def upsert_category( - session: Session, category: Category | None, do_commit=True -) -> CategoryDb | None: - if category is None: - return None - parameters = { - "name": category.name, - "parent": upsert_category(session, category.parent), - } - return _upsert( - session, - CategoryDb, - parameters, - parameters, - do_update=False, - do_commit=do_commit, - ) diff --git a/src/models/__init__.py b/src/models/__init__.py index 446d261..6fe7e34 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -1,10 +1,43 @@ -from src.models.b4w import B4wExport as B4wExport -from src.models.category import Category as Category -from src.models.category_budget import CategoryBudgetDb as CategoryBudgetDb -from src.models.comdirect import ComdirectCreditCard as ComdirectCreditCard -from src.models.comdirect import ComdirectDepot as ComdirectDepot -from src.models.comdirect import ComdirectGiro as ComdirectGiro -from src.models.settings import Account as Account -from src.models.settings import Source as Source -from src.models.transactions import Transaction as Transaction -from src.models.transactions import TransactionDb as TransactionDb +from importlib import import_module +from typing import TYPE_CHECKING, Any + +_EXPORTS = { + "B4wExport": ".b4w", + "Category": ".category", + "CategoryBudgetDb": ".category_budget", + "ComdirectCreditCard": ".comdirect", + "ComdirectDepot": ".comdirect", + "ComdirectGiro": ".comdirect", + "Account": ".settings", + "Source": ".settings", + "Transaction": ".transactions", + "TransactionDb": ".transactions", +} +__all__ = list(_EXPORTS) + + +def __getattr__(name: str) -> Any: + module_name = _EXPORTS.get(name) + if module_name is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + value = getattr(import_module(module_name, __name__), name) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + return sorted([*globals(), *__all__]) + + +if TYPE_CHECKING: + from src.models.b4w import B4wExport as B4wExport + from src.models.category import Category as Category + from src.models.category_budget import CategoryBudgetDb as CategoryBudgetDb + from src.models.comdirect import ComdirectCreditCard as ComdirectCreditCard + from src.models.comdirect import ComdirectDepot as ComdirectDepot + from src.models.comdirect import ComdirectGiro as ComdirectGiro + from src.models.settings import Account as Account + from src.models.settings import Source as Source + from src.models.transactions import Transaction as Transaction + from src.models.transactions import TransactionDb as TransactionDb diff --git a/src/models/category.py b/src/models/category.py index 20dd2d9..970151c 100644 --- a/src/models/category.py +++ b/src/models/category.py @@ -1,6 +1,6 @@ from pydantic import Field, computed_field from sqlalchemy import ForeignKey, String, UniqueConstraint -from sqlalchemy.orm import Mapped, Session, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from src.models.common import BaseModelDb, ExportBaseModel, hashify @@ -54,8 +54,3 @@ class CategoryDb(BaseModelDb): ) children = relationship("CategoryDb", back_populates="parent") budgets = relationship("CategoryBudgetDb", back_populates="category") - - -def add_category(session: Session, category_name: str): - category = Category.from_str(category_name) - session.add(category) diff --git a/tests/test_helper_db.py b/tests/test_helper_db.py index cfda3a2..ea1722e 100644 --- a/tests/test_helper_db.py +++ b/tests/test_helper_db.py @@ -1,7 +1,7 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from src.ingest.common import upsert_category +from src.helper.category import upsert_category from src.models import Category from src.models.category import CategoryDb