add domain entity

main
Сергей Ванюшкин 2024-05-18 00:37:07 +03:00
parent 691acffcfa
commit e5b36a27c3
20 changed files with 255 additions and 167 deletions

16
app_data.json Normal file
View File

@ -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"
}
]

View File

@ -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

View File

@ -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

View File

@ -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"],
)

View File

@ -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,
)
)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"]),
)

View File

@ -0,0 +1,6 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class DomainValueObject:
pass

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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

View File

@ -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:

View File

@ -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,
)
)