From 750f6621d88576671ec35eba98476e4f74fa5711 Mon Sep 17 00:00:00 2001 From: "Christopher A. Flores" Date: Sun, 14 Apr 2024 14:10:15 -0600 Subject: [PATCH] docs: add examples and update documentation --- .gitignore | 3 +- README.md | 192 ++++++++++++------ examples/calculate_age/context/__init__.py | 3 + .../calculate_age/context/calculate_age.py | 18 ++ .../context/register_commands.py | 9 + examples/calculate_age/main.py | 11 + examples/create_user_account/main.py | 80 ++++++++ examples/single_file_example/typed.py | 55 +++++ examples/single_file_example/untyped.py | 30 +++ pyproject.toml | 2 +- 10 files changed, 336 insertions(+), 67 deletions(-) create mode 100644 examples/calculate_age/context/__init__.py create mode 100644 examples/calculate_age/context/calculate_age.py create mode 100644 examples/calculate_age/context/register_commands.py create mode 100644 examples/calculate_age/main.py create mode 100644 examples/create_user_account/main.py create mode 100644 examples/single_file_example/typed.py create mode 100644 examples/single_file_example/untyped.py diff --git a/.gitignore b/.gitignore index 8d45f74..56b7153 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ -dist \ No newline at end of file +dist +.pytest_cache \ No newline at end of file diff --git a/README.md b/README.md index 999631d..853056d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # TurboBus -TurboBus is an opinionated implementation of Command Responsibility Segregation pattern in python. +TurboBus is a package to create software following the Command Responsibility Segregation pattern in python. ## Installation -``` +```bash pip install turbobus ``` @@ -12,122 +12,184 @@ Let's see an example using python typings. You can omit all the typing stuffs if **God Mode ⚡** ```python3 -from dataclasses import dataclass -from typing import TypeAlias +from datetime import date +from turbobus.command import Command, CommandBus, CommandHandler, kw_only_frozen +from turbobus.constants import Provider + +# We need to create a Command class that receives the values that the handler will use +# to execute the command. The Command class is a generic class that receives the return + +# @kw_only_frozen: is a shortcut decorator for @dataclass(kw_only=True, frozen=True) -from turbobus.command import Command, CommandHandler, handler_of -from turbobus.bus import CommandBus +# Command[int]: is a generic class that receives a return_type. +# This is useful to check if the handler is returning the correct type +# And allow the CommandBus to know the return type of the command -LogHandlerType: TypeAlias = "ILogHandler" +@kw_only_frozen +class CalculateAgeCommand(Command[int]): + birthdate: str | date -@dataclass -class LogCommand(Command[LogHandlerType]): - content: str +# We need to create a CommandHandler class that will receive the Command class. +# The handler class must implement the execute method + +# CommandHandler[CalculateAgeCommand]: is a generic class that receives the Command class +# this is useful to check if the handler is implementing the correct command class +class CalculateAgeHandler(CommandHandler[CalculateAgeCommand]): -class ILogHandler(CommandHandler[LogCommand, str]): - ... + # The execute method must receive the Command class and return + # the same type as in the Command class return_type + def execute(self, cmd: CalculateAgeCommand) -> int: + birthdate: date = cmd.birthdate if isinstance(cmd.birthdate, date) else date.fromisoformat(cmd.birthdate) + today = date.today() + age = today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day)) + return age -@handler_of(LogCommand) -class LogHandler(ILogHandler): - def execute(self, cmd: LogCommand) -> str: - return cmd.content + +# We need to register the Command and Handler in the Provider +# This is necessary to allow the CommandBus to find the correct handler +# to execute the command +Provider.set(CalculateAgeCommand, CalculateAgeHandler) if __name__ == '__main__': + # We need to create a CommandBus instance to execute the command bus = CommandBus() + # Here we are executing the CalculateAgeCommand + # if you're using an IDE that supports type hinting + # you'll see that the result variable is inferred as int + # because the CalculateAgeCommand is a generic class + # that receives int as return_type result = bus.execute( - LogCommand('Hello dude!') + CalculateAgeCommand(birthdate='1994-03-09') ) - print(result) # Hello dude + + print(f'You are {result} years old') + ``` -**Human Mode 🥱** +**Human Mode (No types, obviously 🙄)** + +Here's the same example, but without types ```python3 -from dataclasses import dataclass -from typing import TypeAlias +from datetime import date +from turbobus.command import Command, CommandBus, CommandHandler, kw_only_frozen +from turbobus.constants import Provider -from turbobus.command import Command, CommandHandler, handler_of -from turbobus.bus import CommandBus -LogHandlerType: TypeAlias = "ILogHandler" +class CalculateAgeCommand(Command): -@dataclass -class LogCommand(Command[LogHandlerType]): - content: str + def __init__(self, birthdate): + self.birthdate = birthdate -class ILogHandler(CommandHandler[LogCommand, str]): - ... +class CalculateAgeHandler(CommandHandler): + def execute(self, cmd: CalculateAgeCommand): + birthdate = cmd.birthdate if isinstance(cmd.birthdate, date) else date.fromisoformat(cmd.birthdate) -@handler_of(LogCommand) -class LogHandler(ILogHandler): - def execute(self, cmd: LogCommand) -> str: - return cmd.content + today = date.today() + age = today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day)) + return age +Provider.set(CalculateAgeCommand, CalculateAgeHandler) if __name__ == '__main__': bus = CommandBus() result = bus.execute( - LogCommand('Hello dude!') + CalculateAgeCommand(birthdate='1994-03-09') ) - print(result) # Hello dude + + print(f'You are {result} years old') + ``` ## Dependency injection -In many cases we're going to need to inject dependencies to our command handler. To accomplish that we have two important tools: `@injectable_of` decorator and `inject` function. - -With the `@injectable_of` decorator we can specify a class that is implementing the functionalities of the dependency. For example: +In many cases we're going to need to inject dependencies to our command handler. To accomplish that we have the `@inject` decorator. For example: ```python3 -from turbobus.injection import injectable_of, inject -from log.axioma.log import ILogger +from abc import ABC, abstractmethod +from dataclasses import field +import uuid +from turbobus.command import Command, CommandBus, CommandHandler, kw_only_frozen +from turbobus.constants import Provider +from turbobus.injection import inject + +# This is a simple Entity to represent a User +@kw_only_frozen +class UserEntity: + id: uuid.UUID = field(default_factory=uuid.uuid4) + name: str + email: str -class ILogger(ABC): + +# We need to define the repository interface +# to save and retrieve users +class UserRepository(ABC): @abstractmethod - def logger(self, text: str) -> None: - ... + def get_by_id(self, id: uuid.UUID) -> UserEntity | None: + """Get user by id""" + @abstractmethod + def save(self, user: UserEntity) -> None: + """Save user""" -@injectable_of(ILogger) -class Logger: - def logger(self, text: str) -> None: - print(text) +# This is an in-memory implementation of the UserRepository +class UserRepositoryInMemory(UserRepository): + def __init__(self): + self._users: dict[uuid.UUID, UserEntity] = {} -@command(LogCommand) -@dataclass(kw_only=True) -class LogHandler(ILogHandler): + def get_by_id(self, id: uuid.UUID) -> UserEntity | None: + return self._users.get(id) + + def save(self, user: UserEntity) -> None: + self._users[user.id] = user - logger = inject(ILogger) - def execute(self, cmd: LogCommand) -> str: - self.logger.logger(cmd.content) - return cmd.content +# Let's create a command to create a user account +@kw_only_frozen +class CreateUserAccount(Command[None]): + name: str + email: str -``` -As you can see in the example above, we're defining an abstract class with the logger method. Then we're doing the implementation of the `ILogger` and we're indicating that in the `@injectable_of(ILogger)`. +# @inject is used to inject the dependencies +@inject +@kw_only_frozen +class CreateUserAccountHandler(CommandHandler[CreateUserAccount]): -Then, using the `inject` function, TurboBus is going to map that dependency and inject the instance in the attribute. + user_repository: UserRepository + def execute(self, cmd: CreateUserAccount) -> None: + user = UserEntity(name=cmd.name, email=cmd.email) + # It's unnecessary to retrieve the user from the repository + # this is just to demonstrate that the user was saved + self.user_repository.save(user) + user = self.user_repository.get_by_id(user.id) + + if user is None: + raise Exception('User not found') + + print(f'Welcome {user.name}!') -``` -from turbobus.bus import CommandBus -from log.axioma import LogCommand +Provider.set(UserRepository, UserRepositoryInMemory) +Provider.set(CreateUserAccount, CreateUserAccountHandler) -bus = CommandBus() -result = bus.execute( - LogCommand('Hello world') -) -``` \ No newline at end of file +if __name__ == '__main__': + bus = CommandBus() + + bus.execute( + CreateUserAccount(name='Christopher Flores', email='cafadev@outlook.com') + ) + +``` diff --git a/examples/calculate_age/context/__init__.py b/examples/calculate_age/context/__init__.py new file mode 100644 index 0000000..ed95c82 --- /dev/null +++ b/examples/calculate_age/context/__init__.py @@ -0,0 +1,3 @@ +from .register_commands import register + +register() diff --git a/examples/calculate_age/context/calculate_age.py b/examples/calculate_age/context/calculate_age.py new file mode 100644 index 0000000..0ce497d --- /dev/null +++ b/examples/calculate_age/context/calculate_age.py @@ -0,0 +1,18 @@ +from datetime import date +from turbobus.command import Command, CommandHandler, kw_only_frozen + + +@kw_only_frozen +class CalculateAgeCommand(Command[int]): + + birthdate: str | date + + +class CalculateAgeHandler(CommandHandler[CalculateAgeCommand]): + + def execute(self, cmd: CalculateAgeCommand) -> int: + birthdate: date = cmd.birthdate if isinstance(cmd.birthdate, date) else date.fromisoformat(cmd.birthdate) + + today = date.today() + age = today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day)) + return age diff --git a/examples/calculate_age/context/register_commands.py b/examples/calculate_age/context/register_commands.py new file mode 100644 index 0000000..dd3f39d --- /dev/null +++ b/examples/calculate_age/context/register_commands.py @@ -0,0 +1,9 @@ +# Will execute the register function on the __init__ of the module +# To ensure that all commands and handlers are registered + +from examples.calculate_age.context.calculate_age import CalculateAgeCommand, CalculateAgeHandler +from turbobus.constants import Provider + + +def register(): + Provider.set(CalculateAgeCommand, CalculateAgeHandler) diff --git a/examples/calculate_age/main.py b/examples/calculate_age/main.py new file mode 100644 index 0000000..8680ae7 --- /dev/null +++ b/examples/calculate_age/main.py @@ -0,0 +1,11 @@ +from turbobus.command import CommandBus +from context.calculate_age import CalculateAgeCommand + +if __name__ == '__main__': + bus = CommandBus() + + result = bus.execute( + CalculateAgeCommand(birthdate='1994-03-09') + ) + + print(f'You are {result} years old') diff --git a/examples/create_user_account/main.py b/examples/create_user_account/main.py new file mode 100644 index 0000000..82c9d2c --- /dev/null +++ b/examples/create_user_account/main.py @@ -0,0 +1,80 @@ +from abc import ABC, abstractmethod +from dataclasses import field +import uuid +from turbobus.command import Command, CommandBus, CommandHandler, kw_only_frozen +from turbobus.constants import Provider +from turbobus.injection import inject + + +# This is a simple Entity to represent a User +@kw_only_frozen +class UserEntity: + id: uuid.UUID = field(default_factory=uuid.uuid4) + name: str + email: str + + +# We need to define the repository interface +# to save and retrieve users +class UserRepository(ABC): + + @abstractmethod + def get_by_id(self, id: uuid.UUID) -> UserEntity | None: + """Get user by id""" + + @abstractmethod + def save(self, user: UserEntity) -> None: + """Save user""" + + +# This is an in-memory implementation of the UserRepository +class UserRepositoryInMemory(UserRepository): + + def __init__(self): + self._users: dict[uuid.UUID, UserEntity] = {} + + def get_by_id(self, id: uuid.UUID) -> UserEntity | None: + return self._users.get(id) + + def save(self, user: UserEntity) -> None: + self._users[user.id] = user + + +# Let's create a command to create a user account +@kw_only_frozen +class CreateUserAccount(Command[None]): + name: str + email: str + + +# @inject is used to inject the dependencies +@inject +@kw_only_frozen +class CreateUserAccountHandler(CommandHandler[CreateUserAccount]): + + user_repository: UserRepository + + def execute(self, cmd: CreateUserAccount) -> None: + user = UserEntity(name=cmd.name, email=cmd.email) + + # It's unnecessary to retrieve the user from the repository + # this is just to demonstrate that the user was saved + self.user_repository.save(user) + user = self.user_repository.get_by_id(user.id) + + if user is None: + raise Exception('User not found') + + print(f'Welcome {user.name}!') + + +Provider.set(UserRepository, UserRepositoryInMemory) +Provider.set(CreateUserAccount, CreateUserAccountHandler) + + +if __name__ == '__main__': + bus = CommandBus() + + bus.execute( + CreateUserAccount(name='Christopher Flores', email='cafadev@outlook.com') + ) \ No newline at end of file diff --git a/examples/single_file_example/typed.py b/examples/single_file_example/typed.py new file mode 100644 index 0000000..1ae50d3 --- /dev/null +++ b/examples/single_file_example/typed.py @@ -0,0 +1,55 @@ +from datetime import date +from turbobus.command import Command, CommandBus, CommandHandler, kw_only_frozen +from turbobus.constants import Provider + +# We need to create a Command class that receives the values that the handler will use +# to execute the command. The Command class is a generic class that receives the return + +# @kw_only_frozen: is a shortcut decorator for @dataclass(kw_only=True, frozen=True) + +# Command[int]: is a generic class that receives a return_type. +# This is useful to check if the handler is returning the correct type +# And allow the CommandBus to know the return type of the command + +@kw_only_frozen +class CalculateAgeCommand(Command[int]): + birthdate: str | date + + +# We need to create a CommandHandler class that will receive the Command class. +# The handler class must implement the execute method + +# CommandHandler[CalculateAgeCommand]: is a generic class that receives the Command class +# this is useful to check if the handler is implementing the correct command class +class CalculateAgeHandler(CommandHandler[CalculateAgeCommand]): + + # The execute method must receive the Command class and return + # the same type as in the Command class return_type + def execute(self, cmd: CalculateAgeCommand) -> int: + birthdate: date = cmd.birthdate if isinstance(cmd.birthdate, date) else date.fromisoformat(cmd.birthdate) + + today = date.today() + age = today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day)) + return age + + +# We need to register the Command and Handler in the Provider +# This is necessary to allow the CommandBus to find the correct handler +# to execute the command +Provider.set(CalculateAgeCommand, CalculateAgeHandler) + + +if __name__ == '__main__': + # We need to create a CommandBus instance to execute the command + bus = CommandBus() + + # Here we are executing the CalculateAgeCommand + # if you're using an IDE that supports type hinting + # you'll see that the result variable is inferred as int + # because the CalculateAgeCommand is a generic class + # that receives int as return_type + result = bus.execute( + CalculateAgeCommand(birthdate='1994-03-09') + ) + + print(f'You are {result} years old') diff --git a/examples/single_file_example/untyped.py b/examples/single_file_example/untyped.py new file mode 100644 index 0000000..2fe6042 --- /dev/null +++ b/examples/single_file_example/untyped.py @@ -0,0 +1,30 @@ +from datetime import date +from turbobus.command import Command, CommandBus, CommandHandler, kw_only_frozen +from turbobus.constants import Provider + + +class CalculateAgeCommand(Command): + + def __init__(self, birthdate): + self.birthdate = birthdate + + +class CalculateAgeHandler(CommandHandler): + + def execute(self, cmd: CalculateAgeCommand): + birthdate = cmd.birthdate if isinstance(cmd.birthdate, date) else date.fromisoformat(cmd.birthdate) + + today = date.today() + age = today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day)) + return age + +Provider.set(CalculateAgeCommand, CalculateAgeHandler) + +if __name__ == '__main__': + bus = CommandBus() + + result = bus.execute( + CalculateAgeCommand(birthdate='1994-03-09') + ) + + print(f'You are {result} years old') diff --git a/pyproject.toml b/pyproject.toml index 66cab86..6125aac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "turbobus" version = "1.0.0-alpha.0" -description = "TurboBus is an opinionated implementation of Command Responsibility Segregation pattern in python" +description = "TurboBus is an implementation of Command Responsibility Segregation pattern in python" authors = ["Christopher A. Flores "] license = "MIT" readme = "README.md"