Category overhaul #20

Merged
mathias2784 merged 18 commits from category-overhaul into master 2026-10-27 18:08:10 +00:00
9 changed files with 108 additions and 79 deletions
Showing only changes of commit 5cea645062 - Show all commits
+34
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
"""Database helper for application-specific persistence operations."""
+22
View File
@@ -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,
)
+4 -2
View File
@@ -2,7 +2,9 @@ from pathlib import Path
from sqlalchemy.orm import Session 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.ingest.transactions import consolidate_transactions, map_b4w_to_transaction
from src.models.b4w import B4wDb, B4wExport from src.models.b4w import B4wDb, B4wExport
@@ -35,7 +37,7 @@ def upsert_b4w(
"notes": data.notes, "notes": data.notes,
} }
return _upsert( return upsert(
session, session,
B4wDb, B4wDb,
parameters_existing, parameters_existing,
+2 -2
View File
@@ -6,7 +6,7 @@ from typing import cast
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from settings import BANK_ACCOUNTS from settings import BANK_ACCOUNTS
from src.ingest.common import _upsert from src.db_utils import upsert
from src.ingest.transactions import ( from src.ingest.transactions import (
consolidate_transactions, consolidate_transactions,
map_comdirect_giro_to_transaction, map_comdirect_giro_to_transaction,
@@ -137,7 +137,7 @@ def upsert_comdirect_giro(
"text": data.text, "text": data.text,
} }
return _upsert( return upsert(
session, session,
ComdirectGiroDb, ComdirectGiroDb,
parameters_existing, parameters_existing,
-58
View File
@@ -1,65 +1,7 @@
import csv import csv
from pathlib import Path 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): def _reader_raw(path: Path):
with path.open() as fid: with path.open() as fid:
return list(csv.DictReader(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,
)
+33
View File
@@ -1,3 +1,36 @@
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.b4w import B4wExport as B4wExport
from src.models.category import Category as Category from src.models.category import Category as Category
from src.models.category_budget import CategoryBudgetDb as CategoryBudgetDb from src.models.category_budget import CategoryBudgetDb as CategoryBudgetDb
+1 -6
View File
@@ -1,6 +1,6 @@
from pydantic import Field, computed_field from pydantic import Field, computed_field
from sqlalchemy import ForeignKey, String, UniqueConstraint 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 from src.models.common import BaseModelDb, ExportBaseModel, hashify
@@ -54,8 +54,3 @@ class CategoryDb(BaseModelDb):
) )
children = relationship("CategoryDb", back_populates="parent") children = relationship("CategoryDb", back_populates="parent")
budgets = relationship("CategoryBudgetDb", back_populates="category") budgets = relationship("CategoryBudgetDb", back_populates="category")
def add_category(session: Session, category_name: str):
category = Category.from_str(category_name)
session.add(category)
+1 -1
View File
@@ -1,7 +1,7 @@
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session 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 import Category
from src.models.category import CategoryDb from src.models.category import CategoryDb