Adds command line option to add/remove categories

Implements the argument parser, the manager logic and the DB client
methods.
Encapsulates the DbClient connection under the _db attribute on the
manager.

Adds verbose option to enable ORM increased logging.
This commit is contained in:
Luís Murta 2022-12-08 00:09:28 +00:00
parent 882a77d24c
commit 9d33df78a8
Signed by: satprog
GPG Key ID: 169EF1BBD7049F94
6 changed files with 172 additions and 28 deletions

View File

@ -2,4 +2,4 @@ from pfbudget import Manager, run
if __name__ == "__main__": if __name__ == "__main__":
command, args = run() command, args = run()
Manager(command).start(args) Manager(command, args).start()

View File

@ -2,7 +2,7 @@ from pathlib import Path
import argparse import argparse
import re import re
from pfbudget.common.types import Command from pfbudget.common.types import Command, Operation
from pfbudget.core.categories import categorize_data from pfbudget.core.categories import categorize_data
from pfbudget.input.json import JsonParser from pfbudget.input.json import JsonParser
from pfbudget.input.nordigen import NordigenInput from pfbudget.input.nordigen import NordigenInput
@ -40,6 +40,9 @@ def argparser() -> argparse.ArgumentParser:
help.add_argument( help.add_argument(
"-q", "--quiet", action="store_true", help="reduces the amount of verbose" "-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 = argparse.ArgumentParser(add_help=False).add_mutually_exclusive_group()
period.add_argument( period.add_argument(
@ -246,6 +249,56 @@ def argparser() -> argparse.ArgumentParser:
# func=lambda args: manager.parser(JsonParser(vars(args))) # 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 return parser

View File

@ -13,6 +13,15 @@ class Command(Enum):
Unregister = auto() Unregister = auto()
Token = auto() Token = auto()
Renew = auto() Renew = auto()
Category = auto()
class Operation(Enum):
Add = auto()
Remove = auto()
AddGroup = auto()
RemoveGroup = auto()
UpdateGroup = auto()
class TransactionError(Exception): class TransactionError(Exception):

View File

@ -2,7 +2,8 @@ from pfbudget.input.input import Input
from pfbudget.input.nordigen import NordigenClient from pfbudget.input.nordigen import NordigenClient
from pfbudget.input.parsers import parse_data from pfbudget.input.parsers import parse_data
from pfbudget.db.client import DbClient 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.core.categorizer import Categorizer
from pfbudget.utils import convert from pfbudget.utils import convert
@ -10,8 +11,9 @@ from pfbudget.cli.runnable import download, parse
class Manager: class Manager:
def __init__(self, command: Command): def __init__(self, command: Command, args: dict):
self.__command = command self.__command = command
self._args = args
match (command): match (command):
case Command.Init: case Command.Init:
pass pass
@ -29,22 +31,24 @@ class Manager:
pass pass
case Command.Renew: case Command.Renew:
pass 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): match (self.__command):
case Command.Init: case Command.Init:
pass pass
case Command.Parse: case Command.Parse:
# TODO this is a monstrosity, remove when possible # TODO this is a monstrosity, remove when possible
self._db = DbClient(args["database"]) parse(self, self.args)
parse(self, args)
case Command.Download: case Command.Download:
# TODO this is a monstrosity, remove when possible # TODO this is a monstrosity, remove when possible
self._db = DbClient(args["database"]) download(self, self.args)
download(self, args)
case Command.Categorize: case Command.Categorize:
self._db = DbClient(args["database"]) self.categorize(self.args)
self.categorize(args)
case Command.Register: case Command.Register:
# self._db = DbClient(args["database"]) # self._db = DbClient(args["database"])
# self.register(args) # self.register(args)
@ -55,29 +59,72 @@ class Manager:
pass pass
case Command.Token: case Command.Token:
NordigenClient(self).token() NordigenClient(self).token()
case Command.Renew: 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): # def init(self):
# client = DatabaseClient(self.__db) # client = DatabaseClient(self.__db)
# client.init() # client.init()
# def register(self, args: dict): # def register(self):
# bank = Bank(args["bank"][0], "", args["requisition"][0], args["invert"]) # bank = Bank(self.args["bank"][0], "", self.args["requisition"][0], self.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):
# client = DatabaseClient(self.__db) # client = DatabaseClient(self.__db)
# client.unregister_bank(args["bank"][0]) # client.unregister_bank(self.args["bank"][0])
def parser(self, parser: Input): def parser(self, parser: Input):
transactions = parser.parse() transactions = parser.parse()
print(transactions) print(transactions)
# self.add_transactions(transactions) # self.add_transactions(transactions)
# def parse(self, filename: str, args: dict): # def parse(self, filename: str):
# transactions = parse_data(filename, args) # transactions = parse_data(filename, self.args)
# self.add_transactions(transactions) # self.add_transactions(transactions)
# def transactions() -> list[Transaction]: # def transactions() -> list[Transaction]:
@ -86,13 +133,11 @@ class Manager:
def add_transactions(self, transactions): def add_transactions(self, transactions):
with self.db.session() as session: with self.db.session() as session:
session.add(transactions) session.add(transactions)
session.commit()
def categorize(self, args: dict): def categorize(self):
with self.db.session() as session: with self.db.session() as session:
uncategorized = session.uncategorized() uncategorized = session.uncategorized()
Categorizer().categorize(uncategorized) Categorizer().categorize(uncategorized)
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)
@ -103,5 +148,13 @@ class Manager:
return self.db.get_nordigen_banks() return self.db.get_nordigen_banks()
@property @property
def db(self): def db(self) -> DbClient:
return self._db 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

View File

@ -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 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 # import logging
@ -16,8 +16,8 @@ class DbClient:
__sessions: list[Session] __sessions: list[Session]
def __init__(self, url: str) -> None: def __init__(self, url: str, echo=False) -> None:
self._engine = create_engine(url) self._engine = create_engine(url, echo=echo)
def get_transactions(self): def get_transactions(self):
"""¿Non-optimized? get_transactions, will load the entire Transaction""" """¿Non-optimized? get_transactions, will load the entire Transaction"""
@ -65,6 +65,7 @@ class DbClient:
return self return self
def __exit__(self, exc_type, exc_value, exc_tb): def __exit__(self, exc_type, exc_value, exc_tb):
self.commit()
self.__session.close() self.__session.close()
def commit(self): def commit(self):
@ -76,9 +77,32 @@ class DbClient:
def addcategory(self, category: Category): def addcategory(self, category: Category):
self.__session.add(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]: def uncategorized(self) -> list[Transaction]:
stmt = select(Transaction).where(~Transaction.category.has()) stmt = select(Transaction).where(~Transaction.category.has())
return self.__session.scalars(stmt).all() return self.__session.scalars(stmt).all()
def session(self): def session(self) -> ClientSession:
return self.ClientSession(self.engine) return self.ClientSession(self.engine)

View File

@ -108,6 +108,11 @@ class Category(Base):
cascade="all, delete-orphan", passive_deletes=True 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): class TransactionCategory(Base):
__tablename__ = "categorized" __tablename__ = "categorized"