diff --git a/main.py b/main.py index c5f8983..5ed5689 100644 --- a/main.py +++ b/main.py @@ -2,4 +2,4 @@ from pfbudget import Manager, run if __name__ == "__main__": command, args = run() - Manager(command).start(args) + Manager(command, args).start() diff --git a/pfbudget/cli/runnable.py b/pfbudget/cli/runnable.py index d5b1a0e..7e89ef7 100644 --- a/pfbudget/cli/runnable.py +++ b/pfbudget/cli/runnable.py @@ -2,7 +2,7 @@ from pathlib import Path import argparse import re -from pfbudget.common.types import Command +from pfbudget.common.types import Command, Operation from pfbudget.core.categories import categorize_data from pfbudget.input.json import JsonParser from pfbudget.input.nordigen import NordigenInput @@ -40,6 +40,9 @@ def argparser() -> argparse.ArgumentParser: help.add_argument( "-q", "--quiet", action="store_true", help="reduces the amount of verbose" ) + help.add_argument( + "-v", "--verbose", action="store_true", help="increases the amount of verbose" + ) period = argparse.ArgumentParser(add_help=False).add_mutually_exclusive_group() period.add_argument( @@ -246,6 +249,56 @@ def argparser() -> argparse.ArgumentParser: # func=lambda args: manager.parser(JsonParser(vars(args))) # ) + # Add category + p_categories = subparsers.add_parser( + "category", + parents=[help], + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p_categories_commands = p_categories.add_subparsers(dest="command", required=True) + p_categories_add = p_categories_commands.add_parser( + "add", + parents=[help], + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p_categories_add.add_argument("category", nargs="+", type=str) + p_categories_add.add_argument("--group", nargs="?", type=str) + p_categories_add.set_defaults(command=Command.Category, op=Operation.Add) + + p_categories_remove = p_categories_commands.add_parser( + "remove", + parents=[help], + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p_categories_remove.add_argument("category", nargs="+", type=str) + p_categories_remove.add_argument("--group", nargs="?", type=str) + p_categories_remove.set_defaults(command=Command.Category, op=Operation.Remove) + + p_categories_addgroup = p_categories_commands.add_parser( + "addgroup", + parents=[help], + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p_categories_addgroup.add_argument("group", nargs="+", type=str) + p_categories_addgroup.set_defaults(command=Command.Category, op=Operation.AddGroup) + + p_categories_removegroup = p_categories_commands.add_parser( + "removegroup", + parents=[help], + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p_categories_removegroup.add_argument("group", nargs="+", type=str) + p_categories_removegroup.set_defaults(command=Command.Category, op=Operation.RemoveGroup) + + p_categories_updategroup = p_categories_commands.add_parser( + "updategroup", + parents=[help], + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + p_categories_updategroup.add_argument("category", nargs="+", type=str) + p_categories_updategroup.add_argument("--group", nargs=1, type=str) + p_categories_updategroup.set_defaults(command=Command.Category, op=Operation.UpdateGroup) + return parser diff --git a/pfbudget/common/types.py b/pfbudget/common/types.py index c7602c5..829552f 100644 --- a/pfbudget/common/types.py +++ b/pfbudget/common/types.py @@ -13,6 +13,15 @@ class Command(Enum): Unregister = auto() Token = auto() Renew = auto() + Category = auto() + + +class Operation(Enum): + Add = auto() + Remove = auto() + AddGroup = auto() + RemoveGroup = auto() + UpdateGroup = auto() class TransactionError(Exception): diff --git a/pfbudget/core/manager.py b/pfbudget/core/manager.py index a56342e..b964ba8 100644 --- a/pfbudget/core/manager.py +++ b/pfbudget/core/manager.py @@ -2,7 +2,8 @@ from pfbudget.input.input import Input from pfbudget.input.nordigen import NordigenClient from pfbudget.input.parsers import parse_data from pfbudget.db.client import DbClient -from pfbudget.common.types import Command +from pfbudget.db.model import Category, CategoryGroup +from pfbudget.common.types import Command, Operation from pfbudget.core.categorizer import Categorizer from pfbudget.utils import convert @@ -10,8 +11,9 @@ from pfbudget.cli.runnable import download, parse class Manager: - def __init__(self, command: Command): + def __init__(self, command: Command, args: dict): self.__command = command + self._args = args match (command): case Command.Init: pass @@ -29,22 +31,24 @@ class Manager: pass case Command.Renew: pass + case Command.Category: + pass - def start(self, args): + assert "database" in args, "ArgParser didn't include db" + self._db = args["database"] + + def start(self): match (self.__command): case Command.Init: pass case Command.Parse: # TODO this is a monstrosity, remove when possible - self._db = DbClient(args["database"]) - parse(self, args) + parse(self, self.args) case Command.Download: # TODO this is a monstrosity, remove when possible - self._db = DbClient(args["database"]) - download(self, args) + download(self, self.args) case Command.Categorize: - self._db = DbClient(args["database"]) - self.categorize(args) + self.categorize(self.args) case Command.Register: # self._db = DbClient(args["database"]) # self.register(args) @@ -55,29 +59,72 @@ class Manager: pass case Command.Token: NordigenClient(self).token() + case Command.Renew: - NordigenClient(self).requisition(args["name"], args["country"]) + NordigenClient(self).requisition( + self.args["name"], self.args["country"] + ) + + case Command.Category: + assert "op" in self.args, "category operation not defined" + + with self.db.session() as session: + match self.args["op"]: + case Operation.Add: + for category in self.args["category"]: + session.addcategory( + Category(name=category, group=self.args["group"]) + ) + + case Operation.Remove: + session.removecategory( + [ + Category(name=category) + for category in self.args["category"] + ] + ) + + case Operation.UpdateGroup: + session.updategroup( + [ + Category(name=category) + for category in self.args["category"] + ], + self.args["group"][0], + ) + + case Operation.AddGroup: + for group in self.args["group"]: + session.addcategorygroup(CategoryGroup(name=group)) + + case Operation.RemoveGroup: + session.removecategorygroup( + [ + CategoryGroup(name=group) + for group in self.args["group"] + ] + ) # def init(self): # client = DatabaseClient(self.__db) # client.init() - # def register(self, args: dict): - # bank = Bank(args["bank"][0], "", args["requisition"][0], args["invert"]) + # def register(self): + # bank = Bank(self.args["bank"][0], "", self.args["requisition"][0], self.args["invert"]) # client = DatabaseClient(self.__db) # client.register_bank(convert(bank)) - # def unregister(self, args: dict): + # def unregister(self): # client = DatabaseClient(self.__db) - # client.unregister_bank(args["bank"][0]) + # client.unregister_bank(self.args["bank"][0]) def parser(self, parser: Input): transactions = parser.parse() print(transactions) # self.add_transactions(transactions) - # def parse(self, filename: str, args: dict): - # transactions = parse_data(filename, args) + # def parse(self, filename: str): + # transactions = parse_data(filename, self.args) # self.add_transactions(transactions) # def transactions() -> list[Transaction]: @@ -86,13 +133,11 @@ class Manager: def add_transactions(self, transactions): with self.db.session() as session: session.add(transactions) - session.commit() - def categorize(self, args: dict): + def categorize(self): with self.db.session() as session: uncategorized = session.uncategorized() Categorizer().categorize(uncategorized) - session.commit() # def get_bank_by(self, key: str, value: str) -> Bank: # client = DatabaseClient(self.__db) @@ -103,5 +148,13 @@ class Manager: return self.db.get_nordigen_banks() @property - def db(self): - return self._db + def db(self) -> DbClient: + return DbClient(self._db, self.args["verbose"]) + + @db.setter + def db(self, url: str): + self._db = url + + @property + def args(self) -> dict: + return self._args diff --git a/pfbudget/db/client.py b/pfbudget/db/client.py index 0b4d1c5..88bc213 100644 --- a/pfbudget/db/client.py +++ b/pfbudget/db/client.py @@ -1,7 +1,7 @@ -from sqlalchemy import create_engine, select +from sqlalchemy import create_engine, delete, select, update from sqlalchemy.orm import Session, joinedload, selectinload -from pfbudget.db.model import Bank, Category, Transaction +from pfbudget.db.model import Bank, Category, CategoryGroup, Transaction # import logging @@ -16,8 +16,8 @@ class DbClient: __sessions: list[Session] - def __init__(self, url: str) -> None: - self._engine = create_engine(url) + def __init__(self, url: str, echo=False) -> None: + self._engine = create_engine(url, echo=echo) def get_transactions(self): """¿Non-optimized? get_transactions, will load the entire Transaction""" @@ -65,6 +65,7 @@ class DbClient: return self def __exit__(self, exc_type, exc_value, exc_tb): + self.commit() self.__session.close() def commit(self): @@ -76,9 +77,32 @@ class DbClient: def addcategory(self, category: Category): self.__session.add(category) + def removecategory(self, categories: list[Category]): + stmt = delete(Category).where( + Category.name.in_([cat.name for cat in categories]) + ) + self.__session.execute(stmt) + + def updategroup(self, categories: list[Category], group: CategoryGroup): + stmt = ( + update(Category) + .where(Category.name.in_([cat.name for cat in categories])) + .values(group=group) + ) + self.__session.execute(stmt) + + def addcategorygroup(self, group: CategoryGroup): + self.__session.add(group) + + def removecategorygroup(self, groups: list[CategoryGroup]): + stmt = delete(CategoryGroup).where( + CategoryGroup.name.in_([grp.name for grp in groups]) + ) + self.__session.execute(stmt) + def uncategorized(self) -> list[Transaction]: stmt = select(Transaction).where(~Transaction.category.has()) return self.__session.scalars(stmt).all() - def session(self): + def session(self) -> ClientSession: return self.ClientSession(self.engine) diff --git a/pfbudget/db/model.py b/pfbudget/db/model.py index fb900ce..be1ceaa 100644 --- a/pfbudget/db/model.py +++ b/pfbudget/db/model.py @@ -108,6 +108,11 @@ class Category(Base): cascade="all, delete-orphan", passive_deletes=True ) + def __repr__(self) -> str: + return ( + f"Category(name={self.name}, group={self.group}, #rules={len(self.rules)})" + ) + class TransactionCategory(Base): __tablename__ = "categorized"