main structure
parent
155b66f351
commit
691acffcfa
|
@ -1,3 +1,4 @@
|
|||
old_data/
|
||||
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from clifinance.application.dto.expense import ExpenseDTO
|
||||
|
||||
|
||||
class FileDriver(Protocol):
|
||||
@abstractmethod
|
||||
def read(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def write(self, data: list[ExpenseDTO]):
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,36 @@
|
|||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Protocol
|
||||
|
||||
from clifinance.application.dto.balance import Balance
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
|
||||
|
||||
class DataGateway(Protocol):
|
||||
@abstractmethod
|
||||
def get_balance(self) -> Balance:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all_expenses(self) -> list[ExpenseDTO]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def add_expense(self, exp: ExpenseDTO) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def update_expense(self, expense: ExpenseDTO) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all_expenses_by_type(self, type: ExpenseTypeDTO) -> list[ExpenseDTO]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all_expenses_by_date(self, date: datetime) -> list[ExpenseDTO]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all_expenses_by_price(self, price: float) -> list[ExpenseDTO]:
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,41 @@
|
|||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Protocol
|
||||
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
|
||||
|
||||
class FileSession(Protocol):
|
||||
@abstractmethod
|
||||
def commit(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def rollback(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def add(self, expense: ExpenseDTO) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def update(self, expense: ExpenseDTO) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, expense: ExpenseDTO) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get(
|
||||
self,
|
||||
expense_id: int | None,
|
||||
type: ExpenseTypeDTO | None,
|
||||
date: datetime | None,
|
||||
amount: int | None,
|
||||
) -> list[ExpenseDTO]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all(self) -> list[ExpenseDTO]:
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,8 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Balance:
|
||||
balance: float
|
||||
imcome_balance: float
|
||||
expense_balance: float
|
|
@ -0,0 +1,36 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ExpenseTypeDTO(Enum):
|
||||
INCOME = "Income"
|
||||
EXPENSE = "Expense"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpenseDTO:
|
||||
id: int | None
|
||||
type: ExpenseTypeDTO
|
||||
date: datetime
|
||||
amount: float
|
||||
description: str
|
||||
|
||||
def to_json(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": self.type.value,
|
||||
"date": self.date.isoformat(),
|
||||
"amount": self.amount,
|
||||
"description": self.description,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_json(data: dict) -> "ExpenseDTO":
|
||||
return ExpenseDTO(
|
||||
id=data["id"],
|
||||
type=(ExpenseTypeDTO.INCOME if data["type"] == "Income" else ExpenseTypeDTO.EXPENSE),
|
||||
date=datetime.fromisoformat(data["date"]),
|
||||
amount=data["amount"],
|
||||
description=data["description"],
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
import argparse
|
||||
|
||||
from clifinance.application.abstractions.protocols.gateway import DataGateway
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
|
||||
|
||||
class AddExpense:
|
||||
def __init__(
|
||||
self,
|
||||
gateway: DataGateway,
|
||||
) -> None:
|
||||
self.__gateway = gateway
|
||||
|
||||
def __call__(self, args: argparse.Namespace) -> None:
|
||||
self.__gateway.add_expense(
|
||||
exp=ExpenseDTO(
|
||||
id=None,
|
||||
type=(ExpenseTypeDTO.INCOME if args.type == "income" else ExpenseTypeDTO.EXPENSE),
|
||||
date=args.date,
|
||||
amount=args.amount,
|
||||
description=args.description,
|
||||
)
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
from clifinance.application.abstractions.protocols.gateway import DataGateway
|
||||
from clifinance.application.dto.balance import Balance
|
||||
|
||||
|
||||
class GetBalance:
|
||||
def __init__(
|
||||
self,
|
||||
gateway: DataGateway,
|
||||
) -> None:
|
||||
self.__gateway = gateway
|
||||
|
||||
def __call__(self) -> Balance:
|
||||
|
||||
return self.__gateway.get_balance()
|
|
@ -0,0 +1,26 @@
|
|||
import json
|
||||
|
||||
from clifinance.application.dto.expense import ExpenseDTO
|
||||
|
||||
|
||||
class JsonFileDriver:
|
||||
def read(self):
|
||||
try:
|
||||
with open("./app_data.json") as f:
|
||||
data = json.load(f)
|
||||
|
||||
return self.__data_from_json(data)
|
||||
except FileNotFoundError:
|
||||
return list()
|
||||
except json.JSONDecodeError:
|
||||
return list()
|
||||
|
||||
def write(self, data: list[ExpenseDTO]):
|
||||
with open("./app_data.json", "w") as f:
|
||||
json.dump(self.__data_to_json(data), f)
|
||||
|
||||
def __data_to_json(self, data: list[ExpenseDTO]):
|
||||
return [x.to_json() for x in data]
|
||||
|
||||
def __data_from_json(self, data: list[dict]):
|
||||
return [ExpenseDTO.from_json(x) for x in data]
|
|
@ -0,0 +1,41 @@
|
|||
from datetime import datetime
|
||||
|
||||
from clifinance.application.abstractions.protocols.gateway import DataGateway
|
||||
from clifinance.application.abstractions.protocols.session import FileSession
|
||||
from clifinance.application.dto.balance import Balance
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
|
||||
|
||||
class JsonDataGateway(DataGateway):
|
||||
def __init__(self, session: FileSession) -> None:
|
||||
self.session = session
|
||||
|
||||
def get_balance(self) -> Balance:
|
||||
data = self.session.get_all()
|
||||
balance = sum(x.amount for x in data)
|
||||
income = sum(x.amount for x in data if x.type == ExpenseTypeDTO.INCOME)
|
||||
expense = sum(x.amount for x in data if x.type == ExpenseTypeDTO.EXPENSE)
|
||||
return Balance(
|
||||
balance=round(balance, 2),
|
||||
imcome_balance=round(income, 2),
|
||||
expense_balance=round(expense, 2),
|
||||
)
|
||||
|
||||
def get_all_expenses(self) -> list[ExpenseDTO]:
|
||||
return self.session.get_all()
|
||||
|
||||
def add_expense(self, exp: ExpenseDTO) -> None:
|
||||
self.session.add(exp)
|
||||
self.session.commit()
|
||||
|
||||
def update_expense(self, expense: ExpenseDTO) -> None:
|
||||
self.session.update(expense)
|
||||
|
||||
def get_all_expenses_by_type(self, type: ExpenseTypeDTO) -> list[ExpenseDTO]:
|
||||
return [x for x in self.session.get_all() if x.type == type]
|
||||
|
||||
def get_all_expenses_by_date(self, date: datetime) -> list[ExpenseDTO]:
|
||||
return [x for x in self.session.get_all() if x.date == date]
|
||||
|
||||
def get_all_expenses_by_price(self, price: float) -> list[ExpenseDTO]:
|
||||
return [x for x in self.session.get_all() if x.amount == price]
|
|
@ -0,0 +1,85 @@
|
|||
from datetime import datetime
|
||||
|
||||
from clifinance.application.abstractions.protocols.driver import FileDriver
|
||||
from clifinance.application.abstractions.protocols.session import FileSession
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
|
||||
|
||||
class JsonFileSession(FileSession):
|
||||
def __init__(self, driver: FileDriver) -> None:
|
||||
self.driver = driver
|
||||
self.data: list[ExpenseDTO] = driver.read()
|
||||
self.__dirty_new: list[ExpenseDTO] = list()
|
||||
self.__dirty_updated: list[ExpenseDTO] = list()
|
||||
self.__dirty_deleted: list[ExpenseDTO] = list()
|
||||
|
||||
def commit(self) -> None:
|
||||
for expense in self.__dirty_new:
|
||||
self.data.append(expense)
|
||||
|
||||
for expense in self.__dirty_updated:
|
||||
for item in self.data:
|
||||
if item.id == expense.id:
|
||||
item = expense
|
||||
break
|
||||
|
||||
for expense in self.__dirty_deleted:
|
||||
for item in self.data:
|
||||
if item.id == expense.id:
|
||||
del item
|
||||
break
|
||||
self.__dirty_new.clear()
|
||||
self.__dirty_updated.clear()
|
||||
self.__dirty_deleted.clear()
|
||||
|
||||
self.driver.write(self.data)
|
||||
|
||||
def rollback(self) -> None:
|
||||
self.data = self.driver.read()
|
||||
|
||||
def add(self, expense: ExpenseDTO) -> None:
|
||||
if not self.data:
|
||||
last_id = -1
|
||||
else:
|
||||
last_id = max({x.id for x in self.data if x.id is not None})
|
||||
|
||||
self.__dirty_new.append(
|
||||
ExpenseDTO(
|
||||
id=last_id + 1,
|
||||
type=expense.type,
|
||||
date=expense.date,
|
||||
amount=expense.amount,
|
||||
description=expense.description,
|
||||
)
|
||||
)
|
||||
|
||||
def update(self, expense: ExpenseDTO) -> None:
|
||||
self.__dirty_updated.append(expense)
|
||||
|
||||
def delete(self, expense: ExpenseDTO) -> None:
|
||||
self.__dirty_deleted.append(expense)
|
||||
|
||||
def get(
|
||||
self,
|
||||
expense_id: int | None = None,
|
||||
type: ExpenseTypeDTO | None = None,
|
||||
date: datetime | None = None,
|
||||
amount: int | None = None,
|
||||
) -> list[ExpenseDTO]:
|
||||
result: list[ExpenseDTO] = list()
|
||||
for expense in self.data:
|
||||
if expense_id is not None and expense.id != expense_id:
|
||||
continue
|
||||
if type is not None and expense.type != type:
|
||||
continue
|
||||
if date is not None and expense.date != date:
|
||||
continue
|
||||
if amount is not None and expense.amount != amount:
|
||||
continue
|
||||
|
||||
result.append(expense)
|
||||
|
||||
return result
|
||||
|
||||
def get_all(self) -> list[ExpenseDTO]:
|
||||
return self.data
|
|
@ -0,0 +1,13 @@
|
|||
from clifinance.main.container import Container
|
||||
from clifinance.presentation.cli.parsers import provide_parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = provide_parser()
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args=args, ioc=Container())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,48 @@
|
|||
from collections.abc import Callable, Generator
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from inspect import signature
|
||||
from typing import ParamSpec, TypeVar
|
||||
|
||||
from clifinance.application.abstractions.protocols.driver import FileDriver
|
||||
from clifinance.application.abstractions.protocols.gateway import DataGateway
|
||||
from clifinance.application.abstractions.protocols.session import FileSession
|
||||
from clifinance.application.usecases.add_expense import AddExpense
|
||||
from clifinance.application.usecases.get_balance import GetBalance
|
||||
from clifinance.infrastructure.json_driver import JsonFileDriver
|
||||
from clifinance.infrastructure.json_gateway import JsonDataGateway
|
||||
from clifinance.infrastructure.json_session import JsonFileSession
|
||||
|
||||
|
||||
class Container:
|
||||
|
||||
@contextmanager
|
||||
def provide_get_balance(self) -> Generator[GetBalance, None, None]:
|
||||
yield GetBalance(gateway=self.json_gateway())
|
||||
|
||||
@contextmanager
|
||||
def provide_add_expense(self) -> Generator[AddExpense, None, None]:
|
||||
yield AddExpense(gateway=self.json_gateway())
|
||||
|
||||
def json_gateway(self) -> DataGateway:
|
||||
return JsonDataGateway(session=self.get_session())
|
||||
|
||||
def get_session(self) -> FileSession:
|
||||
return JsonFileSession(driver=self.get_driver())
|
||||
|
||||
def get_driver(self) -> FileDriver:
|
||||
return JsonFileDriver()
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def provide_ioc(func: Callable[P, T], ioc: Container) -> partial[T] | None:
|
||||
func_params = signature(func).parameters
|
||||
|
||||
for name, param in func_params.items():
|
||||
if (param.annotation is Container) or (name == "ioc"):
|
||||
return partial(func, **{name: ioc})
|
||||
|
||||
return None
|
|
@ -0,0 +1,27 @@
|
|||
import argparse
|
||||
|
||||
from clifinance.main.container import Container
|
||||
|
||||
|
||||
def get_balance(args: argparse.Namespace, ioc: Container) -> None:
|
||||
with ioc.provide_get_balance() as usecase:
|
||||
print(usecase())
|
||||
|
||||
|
||||
def select_expenses(args: argparse.Namespace, ioc: Container) -> None:
|
||||
print(args)
|
||||
|
||||
|
||||
def add_expense(args: argparse.Namespace, ioc: Container) -> None:
|
||||
if not all(args.__dict__.values()):
|
||||
raise argparse.ArgumentError(
|
||||
argument=None,
|
||||
message="All arguments are required: -t, -d, -a, -D",
|
||||
)
|
||||
if args.type == "income" and args.amount < 0:
|
||||
raise argparse.ArgumentError(argument=None, message="An income cannot have a negative amount.")
|
||||
if args.type == "expense" and args.amount > 0:
|
||||
raise argparse.ArgumentError(argument=None, message="An expense cannot have a positive amount.")
|
||||
|
||||
with ioc.provide_add_expense() as usecase:
|
||||
usecase(args=args)
|
|
@ -0,0 +1,83 @@
|
|||
import argparse
|
||||
import datetime
|
||||
|
||||
from clifinance.presentation.cli.commands import (
|
||||
add_expense,
|
||||
get_balance,
|
||||
select_expenses,
|
||||
)
|
||||
|
||||
|
||||
def get_balance_parser(
|
||||
subparser: argparse._SubParsersAction,
|
||||
) -> argparse.ArgumentParser:
|
||||
balance_parser = subparser.add_parser(name="balance", help="Get balance")
|
||||
|
||||
return balance_parser
|
||||
|
||||
|
||||
def get_select_parser(subparser: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
||||
select_parser = subparser.add_parser(name="select", help="Get expenses by filter")
|
||||
select_parser.add_argument(
|
||||
"-d",
|
||||
"--date",
|
||||
type=datetime.date.fromisoformat,
|
||||
help="Select expenses by date",
|
||||
)
|
||||
select_parser.add_argument(
|
||||
"-p",
|
||||
"--price",
|
||||
type=float,
|
||||
help="Select expenses by price",
|
||||
)
|
||||
select_parser.add_argument(
|
||||
"-t",
|
||||
"--type",
|
||||
type=str,
|
||||
help="Select expenses by type. Available values: income, expense",
|
||||
)
|
||||
return select_parser
|
||||
|
||||
|
||||
def get_add_parser(subparser: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
||||
add_parser = subparser.add_parser(name="add", help="Add new expense")
|
||||
add_parser.add_argument(
|
||||
"-t",
|
||||
"--type",
|
||||
type=str,
|
||||
help="Expense type. Available values: income, expense",
|
||||
)
|
||||
add_parser.add_argument(
|
||||
"-d",
|
||||
"--date",
|
||||
type=datetime.datetime.fromisoformat,
|
||||
help="Expense date",
|
||||
)
|
||||
add_parser.add_argument(
|
||||
"-a",
|
||||
"--amount",
|
||||
type=float,
|
||||
help="Expense price",
|
||||
)
|
||||
add_parser.add_argument(
|
||||
"-D",
|
||||
"--description",
|
||||
type=str,
|
||||
help="Expense description",
|
||||
)
|
||||
return add_parser
|
||||
|
||||
|
||||
def provide_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Cli finance app")
|
||||
subparser = parser.add_subparsers(title="Availeble commands", dest="command")
|
||||
|
||||
balance_parser = get_balance_parser(subparser=subparser)
|
||||
balance_parser.set_defaults(func=get_balance)
|
||||
|
||||
select_parser = get_select_parser(subparser=subparser)
|
||||
select_parser.set_defaults(func=select_expenses)
|
||||
|
||||
add_parser = get_add_parser(subparser=subparser)
|
||||
add_parser.set_defaults(func=add_expense)
|
||||
return parser
|
Loading…
Reference in New Issue