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 abc import abstractmethod
from typing import Protocol from typing import Protocol
from clifinance.application.dto.expense import ExpenseDTO from clifinance.domain.expense.model import Expense
class FileDriver(Protocol): class FileDriver(Protocol):
@ -10,5 +10,5 @@ class FileDriver(Protocol):
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def write(self, data: list[ExpenseDTO]): def write(self, data: list[Expense]):
raise NotImplementedError raise NotImplementedError

View File

@ -2,10 +2,10 @@ from abc import abstractmethod
from datetime import datetime from datetime import datetime
from typing import Protocol 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 @abstractmethod
def commit(self) -> None: def commit(self) -> None:
raise NotImplementedError raise NotImplementedError
@ -15,27 +15,27 @@ class FileSession(Protocol):
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def add(self, expense: ExpenseDTO) -> None: def add(self, expense: Expense) -> None:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def update(self, expense: ExpenseDTO) -> None: def update(self, expense: Expense) -> None:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def delete(self, expense: ExpenseDTO) -> None: def delete(self, expense: Expense) -> None:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def get( def get(
self, self,
expense_id: int | None, expense_id: int | None,
type: ExpenseTypeDTO | None, type: ExpenseType | None,
date: datetime | None, date: datetime | None,
amount: int | None, amount: int | None,
) -> list[ExpenseDTO]: ) -> list[Expense]:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def get_all(self) -> list[ExpenseDTO]: def get_all(self) -> list[Expense]:
raise NotImplementedError raise NotImplementedError

View File

@ -1,36 +1,11 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from enum import Enum
class ExpenseTypeDTO(Enum):
INCOME = "Income"
EXPENSE = "Expense"
@dataclass(frozen=True) @dataclass(frozen=True)
class ExpenseDTO: class ExpenseDTO:
id: int | None id: int | None
type: ExpenseTypeDTO type: str
date: datetime date: datetime
amount: float amount: float
description: str 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.dto.expense import ExpenseDTO
from clifinance.domain.expense.gateway import ExpenseGateway
from clifinance.application.abstractions.protocols.gateway import DataGateway from clifinance.domain.expense.model import Expense
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
class AddExpense: class AddExpense:
def __init__( def __init__(
self, self,
gateway: DataGateway, gateway: ExpenseGateway,
) -> None: ) -> None:
self.__gateway = gateway self.__gateway = gateway
def __call__(self, args: argparse.Namespace) -> None: def __call__(self, expense: ExpenseDTO) -> None:
self.__gateway.add_expense( self.__gateway.add_expense(
exp=ExpenseDTO( exp=Expense.create(
id=None, id=expense.id if expense.id else 1,
type=(ExpenseTypeDTO.INCOME if args.type == "income" else ExpenseTypeDTO.EXPENSE), type=expense.type,
date=args.date, date=expense.date,
amount=args.amount, amount=expense.amount,
description=args.description, description=expense.description,
) )
) )

View File

@ -1,14 +1,14 @@
from clifinance.application.abstractions.protocols.gateway import DataGateway # from clifinance.application.dto.balance import Balance
from clifinance.application.dto.balance import Balance # from clifinance.domain.expense.gateway import ExpenseGateway
#
#
class GetBalance: # class GetBalance:
def __init__( # def __init__(
self, # self,
gateway: DataGateway, # gateway: ExpenseGateway,
) -> None: # ) -> None:
self.__gateway = gateway # self.__gateway = gateway
#
def __call__(self) -> Balance: # def __call__(self) -> Balance:
#
return self.__gateway.get_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 datetime import datetime
from typing import Protocol from typing import Protocol
from clifinance.application.dto.balance import Balance from clifinance.domain.expense.model import Expense, ExpenseType
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
class DataGateway(Protocol): class ExpenseGateway(Protocol):
@abstractmethod @abstractmethod
def get_balance(self) -> Balance: def get_all_expenses(self) -> list[Expense]:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def get_all_expenses(self) -> list[ExpenseDTO]: def add_expense(self, exp: Expense) -> None:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def add_expense(self, exp: ExpenseDTO) -> None: def update_expense(self, expense: Expense) -> None:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def update_expense(self, expense: ExpenseDTO) -> None: def get_all_expenses_by_type(self, type: ExpenseType) -> list[Expense]:
raise NotImplementedError raise NotImplementedError
@abstractmethod @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 raise NotImplementedError
@abstractmethod @abstractmethod
def get_all_expenses_by_date(self, date: datetime) -> list[ExpenseDTO]: def get_all_expenses_by_price(self, price: float) -> list[Expense]:
raise NotImplementedError
@abstractmethod
def get_all_expenses_by_price(self, price: float) -> list[ExpenseDTO]:
raise NotImplementedError 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 import json
from clifinance.application.dto.expense import ExpenseDTO from clifinance.domain.expense.model import Expense
class JsonFileDriver: class JsonFileDriver:
@ -15,12 +15,12 @@ class JsonFileDriver:
except json.JSONDecodeError: except json.JSONDecodeError:
return list() return list()
def write(self, data: list[ExpenseDTO]): def write(self, data: list[Expense]):
with open("./app_data.json", "w") as f: with open("./app_data.json", "w") as f:
json.dump(self.__data_to_json(data), 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] return [x.to_json() for x in data]
def __data_from_json(self, data: list[dict]): 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 datetime import datetime
from clifinance.application.abstractions.protocols.driver import FileDriver from clifinance.application.abstractions.protocols.driver import FileDriver
from clifinance.application.abstractions.protocols.session import FileSession from clifinance.application.abstractions.protocols.session import Session
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO from clifinance.domain.expense.model import Expense, ExpenseType
class JsonFileSession(FileSession): class JsonFileSession(Session):
def __init__(self, driver: FileDriver) -> None: def __init__(self, driver: FileDriver) -> None:
self.driver = driver self.driver = driver
self.data: list[ExpenseDTO] = driver.read() self.data: list[Expense] = driver.read()
self.__dirty_new: list[ExpenseDTO] = list() self.__dirty_new: list[Expense] = list()
self.__dirty_updated: list[ExpenseDTO] = list() self.__dirty_updated: list[Expense] = list()
self.__dirty_deleted: list[ExpenseDTO] = list() self.__dirty_deleted: list[Expense] = list()
def commit(self) -> None: def commit(self) -> None:
for expense in self.__dirty_new: for expense in self.__dirty_new:
@ -37,36 +37,28 @@ class JsonFileSession(FileSession):
def rollback(self) -> None: def rollback(self) -> None:
self.data = self.driver.read() self.data = self.driver.read()
def add(self, expense: ExpenseDTO) -> None: def add(self, expense: Expense) -> None:
if not self.data: # if not self.data:
last_id = -1 # last_id = -1
else: # else:
last_id = max({x.id for x in self.data if x.id is not None}) # 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( def update(self, expense: Expense) -> None:
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) self.__dirty_updated.append(expense)
def delete(self, expense: ExpenseDTO) -> None: def delete(self, expense: Expense) -> None:
self.__dirty_deleted.append(expense) self.__dirty_deleted.append(expense)
def get( def get(
self, self,
expense_id: int | None = None, expense_id: int | None = None,
type: ExpenseTypeDTO | None = None, type: ExpenseType | None = None,
date: datetime | None = None, date: datetime | None = None,
amount: int | None = None, amount: int | None = None,
) -> list[ExpenseDTO]: ) -> list[Expense]:
result: list[ExpenseDTO] = list() result: list[Expense] = list()
for expense in self.data: for expense in self.data:
if expense_id is not None and expense.id != expense_id: if expense_id is not None and expense.id != expense_id:
continue continue
@ -81,5 +73,5 @@ class JsonFileSession(FileSession):
return result return result
def get_all(self) -> list[ExpenseDTO]: def get_all(self) -> list[Expense]:
return self.data return self.data

View File

@ -5,29 +5,30 @@ from inspect import signature
from typing import ParamSpec, TypeVar from typing import ParamSpec, TypeVar
from clifinance.application.abstractions.protocols.driver import FileDriver from clifinance.application.abstractions.protocols.driver import FileDriver
from clifinance.application.abstractions.protocols.gateway import DataGateway from clifinance.application.abstractions.protocols.session import Session
from clifinance.application.abstractions.protocols.session import FileSession
from clifinance.application.usecases.add_expense import AddExpense 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.application.usecases.get_balance import GetBalance
from clifinance.infrastructure.json_gateway import JsonDataGateway from clifinance.domain.expense.gateway import ExpenseGateway
from clifinance.infrastructure.json_session import JsonFileSession from clifinance.infrastructure.persistence.json_driver import JsonFileDriver
from clifinance.infrastructure.persistence.json_gateway import JsonExpenseGateway
from clifinance.infrastructure.session import JsonFileSession
class Container: class Container:
@contextmanager # @contextmanager
def provide_get_balance(self) -> Generator[GetBalance, None, None]: # def provide_get_balance(self) -> Generator[GetBalance, None, None]:
yield GetBalance(gateway=self.json_gateway()) # yield GetBalance(gateway=self.json_gateway())
@contextmanager @contextmanager
def provide_add_expense(self) -> Generator[AddExpense, None, None]: def provide_add_expense(self) -> Generator[AddExpense, None, None]:
yield AddExpense(gateway=self.json_gateway()) yield AddExpense(gateway=self.json_gateway())
def json_gateway(self) -> DataGateway: def json_gateway(self) -> ExpenseGateway:
return JsonDataGateway(session=self.get_session()) return JsonExpenseGateway(session=self.get_session())
def get_session(self) -> FileSession: def get_session(self) -> Session:
return JsonFileSession(driver=self.get_driver()) return JsonFileSession(driver=self.get_driver())
def get_driver(self) -> FileDriver: def get_driver(self) -> FileDriver:

View File

@ -1,11 +1,13 @@
import argparse import argparse
from clifinance.application.dto.expense import ExpenseDTO
from clifinance.main.container import Container from clifinance.main.container import Container
def get_balance(args: argparse.Namespace, ioc: Container) -> None: def get_balance(args: argparse.Namespace, ioc: Container) -> None:
with ioc.provide_get_balance() as usecase: # with ioc.provide_get_balance() as usecase:
print(usecase()) # print(usecase())
print()
def select_expenses(args: argparse.Namespace, ioc: Container) -> None: def select_expenses(args: argparse.Namespace, ioc: Container) -> None:
@ -18,10 +20,14 @@ def add_expense(args: argparse.Namespace, ioc: Container) -> None:
argument=None, argument=None,
message="All arguments are required: -t, -d, -a, -D", 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: 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,
)
)