From e5b36a27c37c0be1a6557cc25a9324d715f8d23f Mon Sep 17 00:00:00 2001 From: Sergey Vanyushkin Date: Sat, 18 May 2024 00:37:07 +0300 Subject: [PATCH] add domain entity --- app_data.json | 16 ++++ .../abstractions/protocols/driver.py | 4 +- .../abstractions/protocols/session.py | 16 ++-- src/clifinance/application/dto/expense.py | 27 +------ .../application/usecases/add_expense.py | 23 +++--- .../application/usecases/get_balance.py | 28 +++---- .../clifinance/domain/__init__.py | 0 src/clifinance/domain/entity.py | 11 +++ src/clifinance/domain/error.py | 8 ++ src/clifinance/domain/expense/__init__.py | 0 src/clifinance/domain/expense/error.py | 17 +++++ .../protocols => domain/expense}/gateway.py | 21 ++---- src/clifinance/domain/expense/model.py | 74 +++++++++++++++++++ src/clifinance/domain/value_obj.py | 6 ++ src/clifinance/infrastructure/json_gateway.py | 41 ---------- .../{ => persistence}/json_driver.py | 8 +- .../persistence/json_gateway.py | 29 ++++++++ .../{json_session.py => session.py} | 48 +++++------- src/clifinance/main/container.py | 25 ++++--- src/clifinance/presentation/cli/commands.py | 20 +++-- 20 files changed, 255 insertions(+), 167 deletions(-) create mode 100644 app_data.json rename __init__.py => src/clifinance/domain/__init__.py (100%) create mode 100644 src/clifinance/domain/entity.py create mode 100644 src/clifinance/domain/error.py create mode 100644 src/clifinance/domain/expense/__init__.py create mode 100644 src/clifinance/domain/expense/error.py rename src/clifinance/{application/abstractions/protocols => domain/expense}/gateway.py (51%) create mode 100644 src/clifinance/domain/expense/model.py create mode 100644 src/clifinance/domain/value_obj.py delete mode 100644 src/clifinance/infrastructure/json_gateway.py rename src/clifinance/infrastructure/{ => persistence}/json_driver.py (71%) create mode 100644 src/clifinance/infrastructure/persistence/json_gateway.py rename src/clifinance/infrastructure/{json_session.py => session.py} (60%) diff --git a/app_data.json b/app_data.json new file mode 100644 index 0000000..c53853f --- /dev/null +++ b/app_data.json @@ -0,0 +1,16 @@ +[ + { + "id": -1, + "type": "Income", + "date": "2000-11-10T00:00:00", + "amount": 100.1, + "description": "sdfsdfsgdhfgh" + }, + { + "id": -1, + "type": "Income", + "date": "2000-11-10T00:00:00", + "amount": 100.1, + "description": "d" + } +] diff --git a/src/clifinance/application/abstractions/protocols/driver.py b/src/clifinance/application/abstractions/protocols/driver.py index 3ac6e17..7bd8e08 100644 --- a/src/clifinance/application/abstractions/protocols/driver.py +++ b/src/clifinance/application/abstractions/protocols/driver.py @@ -1,7 +1,7 @@ from abc import abstractmethod from typing import Protocol -from clifinance.application.dto.expense import ExpenseDTO +from clifinance.domain.expense.model import Expense class FileDriver(Protocol): @@ -10,5 +10,5 @@ class FileDriver(Protocol): raise NotImplementedError @abstractmethod - def write(self, data: list[ExpenseDTO]): + def write(self, data: list[Expense]): raise NotImplementedError diff --git a/src/clifinance/application/abstractions/protocols/session.py b/src/clifinance/application/abstractions/protocols/session.py index 3e5ad35..7f945b1 100644 --- a/src/clifinance/application/abstractions/protocols/session.py +++ b/src/clifinance/application/abstractions/protocols/session.py @@ -2,10 +2,10 @@ from abc import abstractmethod from datetime import datetime from typing import Protocol -from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO +from clifinance.domain.expense.model import Expense, ExpenseType -class FileSession(Protocol): +class Session(Protocol): @abstractmethod def commit(self) -> None: raise NotImplementedError @@ -15,27 +15,27 @@ class FileSession(Protocol): raise NotImplementedError @abstractmethod - def add(self, expense: ExpenseDTO) -> None: + def add(self, expense: Expense) -> None: raise NotImplementedError @abstractmethod - def update(self, expense: ExpenseDTO) -> None: + def update(self, expense: Expense) -> None: raise NotImplementedError @abstractmethod - def delete(self, expense: ExpenseDTO) -> None: + def delete(self, expense: Expense) -> None: raise NotImplementedError @abstractmethod def get( self, expense_id: int | None, - type: ExpenseTypeDTO | None, + type: ExpenseType | None, date: datetime | None, amount: int | None, - ) -> list[ExpenseDTO]: + ) -> list[Expense]: raise NotImplementedError @abstractmethod - def get_all(self) -> list[ExpenseDTO]: + def get_all(self) -> list[Expense]: raise NotImplementedError diff --git a/src/clifinance/application/dto/expense.py b/src/clifinance/application/dto/expense.py index 7aecb1a..e98a9a8 100644 --- a/src/clifinance/application/dto/expense.py +++ b/src/clifinance/application/dto/expense.py @@ -1,36 +1,11 @@ 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 + type: str 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"], - ) diff --git a/src/clifinance/application/usecases/add_expense.py b/src/clifinance/application/usecases/add_expense.py index 3f379bc..78d9114 100644 --- a/src/clifinance/application/usecases/add_expense.py +++ b/src/clifinance/application/usecases/add_expense.py @@ -1,23 +1,22 @@ -import argparse - -from clifinance.application.abstractions.protocols.gateway import DataGateway -from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO +from clifinance.application.dto.expense import ExpenseDTO +from clifinance.domain.expense.gateway import ExpenseGateway +from clifinance.domain.expense.model import Expense class AddExpense: def __init__( self, - gateway: DataGateway, + gateway: ExpenseGateway, ) -> None: self.__gateway = gateway - def __call__(self, args: argparse.Namespace) -> None: + def __call__(self, expense: ExpenseDTO) -> 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, + exp=Expense.create( + id=expense.id if expense.id else 1, + type=expense.type, + date=expense.date, + amount=expense.amount, + description=expense.description, ) ) diff --git a/src/clifinance/application/usecases/get_balance.py b/src/clifinance/application/usecases/get_balance.py index 4fe79a7..6f8149c 100644 --- a/src/clifinance/application/usecases/get_balance.py +++ b/src/clifinance/application/usecases/get_balance.py @@ -1,14 +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() +# from clifinance.application.dto.balance import Balance +# from clifinance.domain.expense.gateway import ExpenseGateway +# +# +# class GetBalance: +# def __init__( +# self, +# gateway: ExpenseGateway, +# ) -> None: +# self.__gateway = gateway +# +# def __call__(self) -> Balance: +# +# return self.__gateway.get_balance() diff --git a/__init__.py b/src/clifinance/domain/__init__.py similarity index 100% rename from __init__.py rename to src/clifinance/domain/__init__.py diff --git a/src/clifinance/domain/entity.py b/src/clifinance/domain/entity.py new file mode 100644 index 0000000..59e3323 --- /dev/null +++ b/src/clifinance/domain/entity.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import Generic, TypeVar + +from clifinance.domain.value_obj import DomainValueObject + +EntityId = TypeVar("EntityId", bound=DomainValueObject) + + +@dataclass +class DomainEntity(Generic[EntityId]): + id: EntityId diff --git a/src/clifinance/domain/error.py b/src/clifinance/domain/error.py new file mode 100644 index 0000000..77379f9 --- /dev/null +++ b/src/clifinance/domain/error.py @@ -0,0 +1,8 @@ +class DomainError(Exception): + def __init__(self, message: str, *args: object) -> None: + self.message = message + super().__init__(*args) + + +class DomainValidationError(DomainError): + pass diff --git a/src/clifinance/domain/expense/__init__.py b/src/clifinance/domain/expense/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/clifinance/domain/expense/error.py b/src/clifinance/domain/expense/error.py new file mode 100644 index 0000000..7a0eed5 --- /dev/null +++ b/src/clifinance/domain/expense/error.py @@ -0,0 +1,17 @@ +from clifinance.domain.error import DomainError, DomainValidationError + + +class ExpenseValidationError(DomainValidationError): + pass + + +class ExpenseError(DomainError): + pass + + +class ExpenseNotFoundError(ExpenseError): + pass + + +class ExpenseAlreadyExistsError(ExpenseError): + pass diff --git a/src/clifinance/application/abstractions/protocols/gateway.py b/src/clifinance/domain/expense/gateway.py similarity index 51% rename from src/clifinance/application/abstractions/protocols/gateway.py rename to src/clifinance/domain/expense/gateway.py index 38a3f39..8d29890 100644 --- a/src/clifinance/application/abstractions/protocols/gateway.py +++ b/src/clifinance/domain/expense/gateway.py @@ -2,35 +2,30 @@ 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 +from clifinance.domain.expense.model import Expense, ExpenseType -class DataGateway(Protocol): +class ExpenseGateway(Protocol): @abstractmethod - def get_balance(self) -> Balance: + def get_all_expenses(self) -> list[Expense]: raise NotImplementedError @abstractmethod - def get_all_expenses(self) -> list[ExpenseDTO]: + def add_expense(self, exp: Expense) -> None: raise NotImplementedError @abstractmethod - def add_expense(self, exp: ExpenseDTO) -> None: + def update_expense(self, expense: Expense) -> None: raise NotImplementedError @abstractmethod - def update_expense(self, expense: ExpenseDTO) -> None: + def get_all_expenses_by_type(self, type: ExpenseType) -> list[Expense]: raise NotImplementedError @abstractmethod - def get_all_expenses_by_type(self, type: ExpenseTypeDTO) -> list[ExpenseDTO]: + def get_all_expenses_by_date(self, date: datetime) -> list[Expense]: 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]: + def get_all_expenses_by_price(self, price: float) -> list[Expense]: raise NotImplementedError diff --git a/src/clifinance/domain/expense/model.py b/src/clifinance/domain/expense/model.py new file mode 100644 index 0000000..2cd279b --- /dev/null +++ b/src/clifinance/domain/expense/model.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from datetime import datetime +from enum import Enum + +from clifinance.domain.entity import DomainEntity, DomainValueObject + + +@dataclass(frozen=True) +class ExpenseId(DomainValueObject): + value: int + + +class ExpenseType(Enum): + INCOME = "Income" + EXPENSE = "Expense" + + +@dataclass(frozen=True) +class ExpenseDate(DomainValueObject): + value: datetime + + def isoformat(self): + return self.value.isoformat() + + @staticmethod + def from_isoformat(date: str) -> "ExpenseDate": + return ExpenseDate(datetime.fromisoformat(date)) + + +@dataclass(frozen=True) +class ExpenseAmount(DomainValueObject): + value: float + + +@dataclass(frozen=True) +class ExpenseDescription(DomainValueObject): + value: str + + +@dataclass +class Expense(DomainEntity[ExpenseId]): + type: ExpenseType + date: ExpenseDate + amount: ExpenseAmount + description: ExpenseDescription + + @staticmethod + def create(id: int, type: str, date: datetime, amount: float, description: str) -> "Expense": + return Expense( + id=ExpenseId(id), + type=ExpenseType(type.title()), + date=ExpenseDate(date), + amount=ExpenseAmount(amount), + description=ExpenseDescription(description), + ) + + def to_json(self) -> dict: + return { + "id": self.id.value, + "type": self.type.value, + "date": self.date.isoformat(), + "amount": self.amount.value, + "description": self.description.value, + } + + @staticmethod + def from_json(data: dict) -> "Expense": + return Expense( + id=ExpenseId(data["id"]), + type=ExpenseType(data["type"]), + date=ExpenseDate.from_isoformat(data["date"]), + amount=ExpenseAmount(data["amount"]), + description=ExpenseDescription(data["description"]), + ) diff --git a/src/clifinance/domain/value_obj.py b/src/clifinance/domain/value_obj.py new file mode 100644 index 0000000..96c1411 --- /dev/null +++ b/src/clifinance/domain/value_obj.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class DomainValueObject: + pass diff --git a/src/clifinance/infrastructure/json_gateway.py b/src/clifinance/infrastructure/json_gateway.py deleted file mode 100644 index 9aa1fcb..0000000 --- a/src/clifinance/infrastructure/json_gateway.py +++ /dev/null @@ -1,41 +0,0 @@ -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] diff --git a/src/clifinance/infrastructure/json_driver.py b/src/clifinance/infrastructure/persistence/json_driver.py similarity index 71% rename from src/clifinance/infrastructure/json_driver.py rename to src/clifinance/infrastructure/persistence/json_driver.py index c8af862..af0a665 100644 --- a/src/clifinance/infrastructure/json_driver.py +++ b/src/clifinance/infrastructure/persistence/json_driver.py @@ -1,6 +1,6 @@ import json -from clifinance.application.dto.expense import ExpenseDTO +from clifinance.domain.expense.model import Expense class JsonFileDriver: @@ -15,12 +15,12 @@ class JsonFileDriver: except json.JSONDecodeError: return list() - def write(self, data: list[ExpenseDTO]): + def write(self, data: list[Expense]): with open("./app_data.json", "w") as f: json.dump(self.__data_to_json(data), f) - def __data_to_json(self, data: list[ExpenseDTO]): + def __data_to_json(self, data: list[Expense]): 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] + return [Expense.from_json(x) for x in data] diff --git a/src/clifinance/infrastructure/persistence/json_gateway.py b/src/clifinance/infrastructure/persistence/json_gateway.py new file mode 100644 index 0000000..b13905e --- /dev/null +++ b/src/clifinance/infrastructure/persistence/json_gateway.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from clifinance.application.abstractions.protocols.session import Session +from clifinance.domain.expense.gateway import ExpenseGateway +from clifinance.domain.expense.model import Expense, ExpenseType + + +class JsonExpenseGateway(ExpenseGateway): + def __init__(self, session: Session) -> None: + self.session = session + + def get_all_expenses(self) -> list[Expense]: + return self.session.get_all() + + def add_expense(self, exp: Expense) -> None: + self.session.add(exp) + self.session.commit() + + def update_expense(self, expense: Expense) -> None: + self.session.update(expense) + + def get_all_expenses_by_type(self, type: ExpenseType) -> list[Expense]: + return [x for x in self.session.get_all() if x.type == type] + + def get_all_expenses_by_date(self, date: datetime) -> list[Expense]: + return [x for x in self.session.get_all() if x.date == date] + + def get_all_expenses_by_price(self, price: float) -> list[Expense]: + return [x for x in self.session.get_all() if x.amount == price] diff --git a/src/clifinance/infrastructure/json_session.py b/src/clifinance/infrastructure/session.py similarity index 60% rename from src/clifinance/infrastructure/json_session.py rename to src/clifinance/infrastructure/session.py index 841c613..691416d 100644 --- a/src/clifinance/infrastructure/json_session.py +++ b/src/clifinance/infrastructure/session.py @@ -1,17 +1,17 @@ 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 +from clifinance.application.abstractions.protocols.session import Session +from clifinance.domain.expense.model import Expense, ExpenseType -class JsonFileSession(FileSession): +class JsonFileSession(Session): 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() + self.data: list[Expense] = driver.read() + self.__dirty_new: list[Expense] = list() + self.__dirty_updated: list[Expense] = list() + self.__dirty_deleted: list[Expense] = list() def commit(self) -> None: for expense in self.__dirty_new: @@ -37,36 +37,28 @@ class JsonFileSession(FileSession): 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}) + def add(self, expense: Expense) -> 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(expense) - 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: + def update(self, expense: Expense) -> None: self.__dirty_updated.append(expense) - def delete(self, expense: ExpenseDTO) -> None: + def delete(self, expense: Expense) -> None: self.__dirty_deleted.append(expense) def get( self, expense_id: int | None = None, - type: ExpenseTypeDTO | None = None, + type: ExpenseType | None = None, date: datetime | None = None, amount: int | None = None, - ) -> list[ExpenseDTO]: - result: list[ExpenseDTO] = list() + ) -> list[Expense]: + result: list[Expense] = list() for expense in self.data: if expense_id is not None and expense.id != expense_id: continue @@ -81,5 +73,5 @@ class JsonFileSession(FileSession): return result - def get_all(self) -> list[ExpenseDTO]: + def get_all(self) -> list[Expense]: return self.data diff --git a/src/clifinance/main/container.py b/src/clifinance/main/container.py index 81f105a..c55222d 100644 --- a/src/clifinance/main/container.py +++ b/src/clifinance/main/container.py @@ -5,29 +5,30 @@ 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.abstractions.protocols.session import Session 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 + +# from clifinance.application.usecases.get_balance import GetBalance +from clifinance.domain.expense.gateway import ExpenseGateway +from clifinance.infrastructure.persistence.json_driver import JsonFileDriver +from clifinance.infrastructure.persistence.json_gateway import JsonExpenseGateway +from clifinance.infrastructure.session import JsonFileSession class Container: - @contextmanager - def provide_get_balance(self) -> Generator[GetBalance, None, None]: - yield GetBalance(gateway=self.json_gateway()) + # @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 json_gateway(self) -> ExpenseGateway: + return JsonExpenseGateway(session=self.get_session()) - def get_session(self) -> FileSession: + def get_session(self) -> Session: return JsonFileSession(driver=self.get_driver()) def get_driver(self) -> FileDriver: diff --git a/src/clifinance/presentation/cli/commands.py b/src/clifinance/presentation/cli/commands.py index bc8c14f..fdde7fb 100644 --- a/src/clifinance/presentation/cli/commands.py +++ b/src/clifinance/presentation/cli/commands.py @@ -1,11 +1,13 @@ import argparse +from clifinance.application.dto.expense import ExpenseDTO from clifinance.main.container import Container def get_balance(args: argparse.Namespace, ioc: Container) -> None: - with ioc.provide_get_balance() as usecase: - print(usecase()) + # with ioc.provide_get_balance() as usecase: + # print(usecase()) + print() def select_expenses(args: argparse.Namespace, ioc: Container) -> None: @@ -18,10 +20,14 @@ def add_expense(args: argparse.Namespace, ioc: Container) -> None: 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) + usecase( + expense=ExpenseDTO( + id=args.id if args.__dict__.get("id") else -1, + type=args.type, + date=args.date, + amount=args.amount, + description=args.description, + ) + )