From ec22b5e5bdb054b1d78600189d2f1f07f9b223b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Mon, 8 May 2023 20:09:37 +0100 Subject: [PATCH] [DB][Refactor] Compact the category selector The `CategorySelector` was possibly added to be incremented with other attributes. However, since none other that the selector enum is used at the moment, it is only adding unnecessary cluter. The category selector value is moved to the parent `TransactionCategory`. --- .../8623e709e111_compact_category_selector.py | 74 +++++++++++++++++++ .../b599dafcf468_selector_type_name_change.py | 46 ++++++++++++ pfbudget/cli/interactive.py | 9 +-- pfbudget/core/manager.py | 8 +- pfbudget/db/model.py | 38 +++------- pfbudget/transform/categorizer.py | 3 +- pfbudget/transform/nullifier.py | 3 +- pfbudget/utils/serializer.py | 2 +- tests/mocks/transactions.py | 9 +-- tests/test_database.py | 8 +- tests/test_transform.py | 17 +---- 11 files changed, 147 insertions(+), 70 deletions(-) create mode 100644 alembic/versions/8623e709e111_compact_category_selector.py create mode 100644 alembic/versions/b599dafcf468_selector_type_name_change.py diff --git a/alembic/versions/8623e709e111_compact_category_selector.py b/alembic/versions/8623e709e111_compact_category_selector.py new file mode 100644 index 0000000..62d824b --- /dev/null +++ b/alembic/versions/8623e709e111_compact_category_selector.py @@ -0,0 +1,74 @@ +"""Compact category selector + +Revision ID: 8623e709e111 +Revises: ce68ee15e5d2 +Create Date: 2023-05-08 19:00:51.063240+00:00 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "8623e709e111" +down_revision = "ce68ee15e5d2" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("category_selectors", schema="pfbudget") + op.add_column( + "transactions_categorized", + sa.Column( + "selector", + sa.Enum( + "unknown", + "nullifier", + "vacations", + "rules", + "algorithm", + "manual", + name="selector_t", + schema="pfbudget", + inherit_schema=True, + ), + nullable=False, + ), + schema="pfbudget", + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("transactions_categorized", "selector", schema="pfbudget") + op.create_table( + "category_selectors", + sa.Column("id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "selector", + postgresql.ENUM( + "unknown", + "nullifier", + "vacations", + "rules", + "algorithm", + "manual", + name="selector_t", + schema="pfbudget", + ), + autoincrement=False, + nullable=False, + ), + sa.ForeignKeyConstraint( + ["id"], + ["pfbudget.transactions_categorized.id"], + name="fk_category_selectors_id_transactions_categorized", + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name="pk_category_selectors"), + schema="pfbudget", + ) + # ### end Alembic commands ### diff --git a/alembic/versions/b599dafcf468_selector_type_name_change.py b/alembic/versions/b599dafcf468_selector_type_name_change.py new file mode 100644 index 0000000..b510fc4 --- /dev/null +++ b/alembic/versions/b599dafcf468_selector_type_name_change.py @@ -0,0 +1,46 @@ +"""Selector type name change + +Revision ID: b599dafcf468 +Revises: 8623e709e111 +Create Date: 2023-05-08 19:46:20.661214+00:00 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "b599dafcf468" +down_revision = "8623e709e111" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TYPE pfbudget.categoryselector + AS ENUM ('unknown', 'nullifier', 'vacations', 'rules', 'algorithm', 'manual') + """ + ) + op.execute( + """ALTER TABLE pfbudget.transactions_categorized + ALTER COLUMN selector TYPE pfbudget.categoryselector + USING selector::text::pfbudget.categoryselector + """ + ) + op.execute("DROP TYPE pfbudget.selector_t") + + +def downgrade() -> None: + op.execute( + """ + CREATE TYPE pfbudget.selector_t + AS ENUM ('unknown', 'nullifier', 'vacations', 'rules', 'algorithm', 'manual') + """ + ) + op.execute( + """ALTER TABLE pfbudget.transactions_categorized + ALTER COLUMN selector TYPE pfbudget.selector_t + USING selector::text::pfbudget.selector_t + """ + ) + op.execute("DROP TYPE pfbudget.categoryselector") diff --git a/pfbudget/cli/interactive.py b/pfbudget/cli/interactive.py index 9696f17..43c4e5e 100644 --- a/pfbudget/cli/interactive.py +++ b/pfbudget/cli/interactive.py @@ -3,9 +3,8 @@ import decimal from ..core.manager import Manager from ..db.model import ( Category, - CategorySelector, Note, - Selector_T, + CategorySelector, SplitTransaction, Tag, Transaction, @@ -16,7 +15,7 @@ from ..db.model import ( class Interactive: help = "category(:tag)/split/note:/skip/quit" - selector = Selector_T.manual + selector = CategorySelector.manual def __init__(self, manager: Manager) -> None: self.manager = manager @@ -79,9 +78,7 @@ class Interactive: if len(ct) > 1: tags = ct[1:] - next.category = TransactionCategory( - category, CategorySelector(self.selector) - ) + next.category = TransactionCategory(category, self.selector) for tag in tags: if tag not in [t.name for t in self.tags]: session.insert([Tag(tag)]) diff --git a/pfbudget/core/manager.py b/pfbudget/core/manager.py index c968527..166757f 100644 --- a/pfbudget/core/manager.py +++ b/pfbudget/core/manager.py @@ -13,12 +13,11 @@ from pfbudget.db.model import ( CategoryGroup, CategoryRule, CategorySchedule, - CategorySelector, Link, MoneyTransaction, Nordigen, Rule, - Selector_T, + CategorySelector, SplitTransaction, Tag, TagRule, @@ -270,8 +269,7 @@ class Manager: if category := row.pop("category", None): transaction.category = TransactionCategory( - category["name"], - CategorySelector(category["selector"]["selector"]), + category["name"], category["selector"]["selector"] ) transactions.append(transaction) @@ -348,7 +346,7 @@ class Manager: return parse_data(filename, args) def askcategory(self, transaction: Transaction): - selector = CategorySelector(Selector_T.manual) + selector = CategorySelector.manual categories = self.database.select(Category) diff --git a/pfbudget/db/model.py b/pfbudget/db/model.py index bb955e5..1bdfe33 100644 --- a/pfbudget/db/model.py +++ b/pfbudget/db/model.py @@ -36,6 +36,10 @@ class Base(MappedAsDataclass, DeclarativeBase): }, ) + type_annotation_map = { + enum.Enum: Enum(enum.Enum, create_constraint=True, inherit_schema=True), + } + class AccountType(enum.Enum): checking = enum.auto() @@ -205,7 +209,7 @@ catfk = Annotated[ ] -class Selector_T(enum.Enum): +class CategorySelector(enum.Enum): unknown = enum.auto() nullifier = enum.auto() vacations = enum.auto() @@ -220,9 +224,7 @@ class TransactionCategory(Base, Export): id: Mapped[idfk] = mapped_column(primary_key=True, init=False) name: Mapped[catfk] - selector: Mapped[CategorySelector] = relationship( - cascade="all, delete-orphan", default=Selector_T.unknown, lazy="joined" - ) + selector: Mapped[CategorySelector] = mapped_column(default=CategorySelector.unknown) transaction: Mapped[Transaction] = relationship( back_populates="category", init=False, compare=False @@ -231,7 +233,7 @@ class TransactionCategory(Base, Export): @property def format(self): return dict( - name=self.name, selector=self.selector.format if self.selector else None + name=self.name, selector=self.selector.name ) @@ -281,28 +283,6 @@ class TransactionTag(Base, Export, unsafe_hash=True): return dict(tag=self.tag) -categoryselector = Annotated[ - Selector_T, - mapped_column(Enum(Selector_T, inherit_schema=True)), -] - - -class CategorySelector(Base, Export): - __tablename__ = "category_selectors" - - id: Mapped[int] = mapped_column( - BigInteger, - ForeignKey(TransactionCategory.id, ondelete="CASCADE"), - primary_key=True, - init=False, - ) - selector: Mapped[categoryselector] = mapped_column(default=Selector_T.unknown) - - @property - def format(self): - return dict(selector=self.selector) - - class Period(enum.Enum): daily = "daily" weekly = "weekly" @@ -310,7 +290,9 @@ class Period(enum.Enum): yearly = "yearly" -scheduleperiod = Annotated[Selector_T, mapped_column(Enum(Period, inherit_schema=True))] +scheduleperiod = Annotated[ + CategorySelector, mapped_column(Enum(Period, inherit_schema=True)) +] class CategorySchedule(Base, Export): diff --git a/pfbudget/transform/categorizer.py b/pfbudget/transform/categorizer.py index c66fcf5..91aa110 100644 --- a/pfbudget/transform/categorizer.py +++ b/pfbudget/transform/categorizer.py @@ -4,7 +4,6 @@ from typing import Iterable, Sequence from pfbudget.db.model import ( CategoryRule, CategorySelector, - Selector_T, Transaction, TransactionCategory, ) @@ -32,5 +31,5 @@ class Categorizer(Transformer): continue transaction.category = TransactionCategory( - rule.name, CategorySelector(Selector_T.rules) + rule.name, CategorySelector.rules ) diff --git a/pfbudget/transform/nullifier.py b/pfbudget/transform/nullifier.py index b25b9b4..4be4909 100644 --- a/pfbudget/transform/nullifier.py +++ b/pfbudget/transform/nullifier.py @@ -6,7 +6,6 @@ from .exceptions import MoreThanOneMatchError from .transform import Transformer from pfbudget.db.model import ( CategorySelector, - Selector_T, Transaction, TransactionCategory, ) @@ -89,6 +88,6 @@ class Nullifier(Transformer): def _nullify(self, transaction: Transaction) -> Transaction: transaction.category = TransactionCategory( - "null", selector=CategorySelector(Selector_T.nullifier) + "null", selector=CategorySelector.nullifier ) return transaction diff --git a/pfbudget/utils/serializer.py b/pfbudget/utils/serializer.py index adef7d4..35d3657 100644 --- a/pfbudget/utils/serializer.py +++ b/pfbudget/utils/serializer.py @@ -21,7 +21,7 @@ def _(obj: Transaction) -> Mapping[str, Any]: if obj.category: category = { "name": obj.category.name, - "selector": str(obj.category.selector.selector) + "selector": str(obj.category.selector) if obj.category.selector else None, } diff --git a/tests/mocks/transactions.py b/tests/mocks/transactions.py index c0f4fa9..a540fbf 100644 --- a/tests/mocks/transactions.py +++ b/tests/mocks/transactions.py @@ -3,7 +3,6 @@ from decimal import Decimal from pfbudget.db.model import ( CategorySelector, - Selector_T, Transaction, TransactionCategory, ) @@ -18,16 +17,12 @@ simple_transformed = [ date(2023, 1, 1), "", Decimal("-10"), - category=TransactionCategory( - "category#1", CategorySelector(Selector_T.algorithm) - ), + category=TransactionCategory("category#1", CategorySelector.algorithm), ), Transaction( date(2023, 1, 2), "", Decimal("-50"), - category=TransactionCategory( - "category#2", CategorySelector(Selector_T.algorithm) - ), + category=TransactionCategory("category#2", CategorySelector.algorithm), ), ] diff --git a/tests/test_database.py b/tests/test_database.py index 8e904b1..c33b219 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -7,9 +7,8 @@ from pfbudget.db.model import ( AccountType, Bank, Base, - CategorySelector, Nordigen, - Selector_T, + CategorySelector, Transaction, TransactionCategory, ) @@ -43,9 +42,7 @@ def transactions(client: Client) -> list[Transaction]: date(2023, 1, 1), "", Decimal("-10"), - category=TransactionCategory( - "category", CategorySelector(Selector_T.algorithm) - ), + category=TransactionCategory("category", CategorySelector.algorithm), ), Transaction(date(2023, 1, 2), "", Decimal("-50")), ] @@ -55,7 +52,6 @@ def transactions(client: Client) -> list[Transaction]: transaction.id = i + 1 if transaction.category: transaction.category.id = 1 - transaction.category.selector.id = 1 return transactions diff --git a/tests/test_transform.py b/tests/test_transform.py index 6d24662..f27c740 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -7,7 +7,6 @@ from pfbudget.db.model import ( BankTransaction, CategoryRule, CategorySelector, - Selector_T, TransactionCategory, TransactionTag, ) @@ -31,9 +30,7 @@ class TestTransform: transactions = categorizer.transform(transactions) for t in transactions: - assert t.category == TransactionCategory( - "null", CategorySelector(Selector_T.nullifier) - ) + assert t.category == TransactionCategory("null", CategorySelector.nullifier) def test_nullifier_inplace(self): transactions = [ @@ -48,9 +45,7 @@ class TestTransform: categorizer.transform_inplace(transactions) for t in transactions: - assert t.category == TransactionCategory( - "null", CategorySelector(Selector_T.nullifier) - ) + assert t.category == TransactionCategory("null", CategorySelector.nullifier) def test_nullifier_with_rules(self): transactions = [ @@ -74,9 +69,7 @@ class TestTransform: transactions = categorizer.transform(transactions) for t in transactions: - assert t.category == TransactionCategory( - "null", CategorySelector(Selector_T.nullifier) - ) + assert t.category == TransactionCategory("null", CategorySelector.nullifier) def test_tagger(self): transactions = [ @@ -104,6 +97,4 @@ class TestTransform: transactions = categorizer.transform(transactions) for t in transactions: - assert t.category == TransactionCategory( - "cat#1", CategorySelector(Selector_T.rules) - ) + assert t.category == TransactionCategory("cat#1", CategorySelector.rules)