From 70a95ea1eaa7339f0ab84afc6a44ae82831aa5bc Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 22 Jul 2023 20:12:41 +1000 Subject: [PATCH] Delete generics and tidy up --- src/clapy/common.py | 9 +++-- src/clapy/dependency_injection.py | 13 +++---- src/clapy/engine.py | 8 ++-- src/clapy/generics.py | 8 ---- src/clapy/outputs.py | 51 ++++++++++++++++++++++--- src/clapy/pipeline.py | 62 +++++++++++++++---------------- src/clapy/services.py | 16 ++++---- 7 files changed, 100 insertions(+), 67 deletions(-) delete mode 100644 src/clapy/generics.py diff --git a/src/clapy/common.py b/src/clapy/common.py index cc8b617..a02da65 100644 --- a/src/clapy/common.py +++ b/src/clapy/common.py @@ -2,7 +2,7 @@ import inspect import os import re -from typing import List +from typing import List, Tuple DIR_EXCLUSIONS = [r"__pycache__"] @@ -67,7 +67,10 @@ def import_class_by_namespace(namespace: str) -> type: @staticmethod - def get_all_classes(location, directory_exclusion_patterns, file_exclusion_patterns) -> List[(object, str)]: + def get_all_classes( + location: str, + directory_exclusion_patterns: List[str], + file_exclusion_patterns: List[str]) -> List[Tuple[object, str]]: ''' Summary ------- @@ -91,7 +94,7 @@ def get_all_classes(location, directory_exclusion_patterns, file_exclusion_patte _Directories[:] = [_Dir for _Dir in _Directories if not re.match(_ExclusionPattern, _Dir)] for _ExclusionPattern in file_exclusion_patterns + FILE_EXCLUSIONS: - _Files[:] = [_File for _File in _Files if not re.match(_ExclusionPattern, _Files)] + _Files[:] = [_File for _File in _Files if not re.match(_ExclusionPattern, _File)] for _File in _Files: _Namespace = _Root.replace('/', '.').lstrip(".") + "." + _File[:-3] diff --git a/src/clapy/dependency_injection.py b/src/clapy/dependency_injection.py index de05b31..c0d6cee 100644 --- a/src/clapy/dependency_injection.py +++ b/src/clapy/dependency_injection.py @@ -7,7 +7,6 @@ from .common import Common from .engine import Engine, PipelineFactory, UseCaseInvoker from .exceptions import DuplicateServiceError -from .generics import TServiceType from .pipeline import IPipe from .services import IPipelineFactory, IServiceProvider, IUseCaseInvoker @@ -22,7 +21,7 @@ class DependencyInjectorServiceProvider(IServiceProvider): def __init__(self): self._container = containers.DeclarativeContainer() - def get_service(self, service: Type[TServiceType]) -> TServiceType: + def get_service(self, service: type) -> object: ''' Summary ------- @@ -53,9 +52,9 @@ def get_service(self, service: Type[TServiceType]) -> TServiceType: def register_service( self, - provider_method: Type, - concrete_type: Type[TServiceType], - interface_type: Optional[Type[TServiceType]] = None, *args) -> None: + provider_method: type, + concrete_type: type, + interface_type: Optional[type] = None, *args) -> None: ''' Summary ------- @@ -162,7 +161,7 @@ def construct_usecase_invoker( self.register_service(providers.Factory, UseCaseInvoker, IUseCaseInvoker) return self.get_service(IUseCaseInvoker) - def _try_generate_service_name(self, service: Type[TServiceType]) -> Tuple[str, bool]: + def _try_generate_service_name(self, service: type) -> Tuple[str, bool]: ''' Summary ------- @@ -185,7 +184,7 @@ def _try_generate_service_name(self, service: Type[TServiceType]) -> Tuple[str, return _TypeMatch.group().replace('.', '_'), True - def _has_service(self, service: Type[TServiceType]) -> bool: + def _has_service(self, service: type) -> bool: ''' Summary ------- diff --git a/src/clapy/engine.py b/src/clapy/engine.py index 6fcd72c..16a08ca 100644 --- a/src/clapy/engine.py +++ b/src/clapy/engine.py @@ -3,7 +3,7 @@ from .common import Common from .exceptions import PipeConfigurationError -from .generics import TInputPort, TOutputPort +from .outputs import IOutputPort from .pipeline import (InputPort, IPipe, PipeConfiguration, PipeConfigurationOption) from .services import IPipelineFactory, IServiceProvider, IUseCaseInvoker @@ -18,7 +18,7 @@ def __init__(self, service_provider: IServiceProvider, usecase_registry: Dict[st async def create_pipeline_async( self, - input_port: TInputPort, + input_port: InputPort, pipeline_configuration: List[PipeConfiguration]) -> List[Type[IPipe]]: ''' Summary @@ -77,8 +77,8 @@ def __init__(self, pipeline_factory: IPipelineFactory): async def invoke_usecase_async( self, - input_port: TInputPort, - output_port: TOutputPort, + input_port: InputPort, + output_port: IOutputPort, pipeline_configuration: List[PipeConfiguration]) -> None: ''' Summary diff --git a/src/clapy/generics.py b/src/clapy/generics.py deleted file mode 100644 index 8646e82..0000000 --- a/src/clapy/generics.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import TypeVar - - -TAuthorisationFailure = TypeVar("TAuthorisationFailure") -TInputPort = TypeVar("TInputPort") -TOutputPort = TypeVar("TOutputPort") -TServiceType = TypeVar("TServiceType") -TValidationFailure = TypeVar("TValidationFailure") diff --git a/src/clapy/outputs.py b/src/clapy/outputs.py index 52f8638..b2bfe46 100644 --- a/src/clapy/outputs.py +++ b/src/clapy/outputs.py @@ -1,7 +1,46 @@ from abc import ABC, abstractmethod -from typing import Generic +from typing import Dict, List, Optional -from .generics import TAuthorisationFailure, TValidationFailure + +class AuthorisationResult: + '''An authorisation result from an authorisation enforcer.''' + + def __init__(self, reason: Optional[str] = None) -> None: + self.reason = reason + + +class ValidationResult: + '''A validation result from a validator.''' + + def __init__( + self, + errors: Optional[Dict[str, List[str]]] = {}, + summary: Optional[str] = None) -> None: + self.errors = errors + self.summary = summary + + @classmethod + def from_error(cls, property: str, error_message: str) -> 'ValidationResult': + '''#TODO''' + instance = cls() + instance.add_error(property, error_message) + return instance + + @classmethod + def from_summary(cls, summary: str) -> 'ValidationResult': + '''#TODO''' + instance = cls() + instance.summary = summary + return instance + + def add_error(self, property: str, error_message: str) -> None: + '''#TODO''' + self.errors.setdefault(property.__name__, []).append(error_message) + + +class IOutputPort(ABC): + '''Marks a class as a use case output port.''' + pass class IAuthenticationOutputPort(ABC): @@ -13,19 +52,19 @@ async def present_unauthenticated_async() -> None: pass -class IAuthorisationOutputPort(Generic[TAuthorisationFailure], ABC): +class IAuthorisationOutputPort(ABC): '''An output port for when authorisation is required by the use case.''' @abstractmethod - async def present_unauthorised_async(self, authorisation_failure: TAuthorisationFailure) -> None: + async def present_unauthorised_async(self, authorisation_failure: AuthorisationResult) -> None: '''Presents an authorisation failure.''' pass -class IValidationOutputPort(Generic[TValidationFailure], ABC): +class IValidationOutputPort(ABC): '''An output port for when validation is required by the use case.''' @abstractmethod - async def present_validation_failure_async(self, validation_failure: TValidationFailure) -> None: + async def present_validation_failure_async(self, validation_failure: ValidationResult) -> None: '''Presents a validation failure.''' pass diff --git a/src/clapy/pipeline.py b/src/clapy/pipeline.py index 8811ab0..aff76d2 100644 --- a/src/clapy/pipeline.py +++ b/src/clapy/pipeline.py @@ -1,39 +1,39 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import Any, Coroutine, Generic, NamedTuple, Type, Union +from typing import NamedTuple, Type -from .outputs import IValidationOutputPort -from .generics import TInputPort, TOutputPort +from .outputs import IOutputPort, IValidationOutputPort, ValidationResult -class IPipe(Generic[TInputPort, TOutputPort], ABC): - '''Marks a class as a pipe. A pipe is a class that must have an execution method and a priority.''' +class InputPort: + '''Marks a class as an input port (not an implementation of IPipe). The entry point for all use cases.''' + pass + + +class IPipe(InputPort, IOutputPort, ABC): + '''Marks a class as a pipe. A pipe is a class that has an execution method and reports on failures.''' def __init__(self) -> None: self._has_failures = False @abstractmethod - async def execute_async(self, input_port: TInputPort, output_port: TOutputPort) -> Union[Coroutine, None]: + async def execute_async(self, input_port: InputPort, output_port: IOutputPort) -> None: ''' Summary ------- - Defines the behaviour of the pipe when executed. Must return either a coroutine - function (an output port method), or no result. + Defines the behaviour of the pipe when executed. Parameters ---------- `input_port` The input of the use case to be processed\n - `output_port` The interface containing output methods to return as a result of the pipe's execution - - Returns - ------- - `Coroutine` (a method of the output port) if the pipe has a result to return, otherwise `None`. + `output_port` The interface containing methods to output the result of the pipe's execution ''' pass @property def has_failures(self) -> bool: + '''Determines whether or not a failure has occurred during the pipe's execution.''' return self._has_failures @has_failures.setter @@ -48,7 +48,7 @@ class PipeConfigurationOption(Enum): '''If found from searching the use case pipes, the pipe will be added, otherwise it is ignored.''' INSERT = "INSERT" - '''Will insert the provided pipe at the specified location.''' + '''Will insert the pipe at the specified location, regardless of its presence within the defined used case.''' class PipeConfiguration(NamedTuple): @@ -57,8 +57,10 @@ class PipeConfiguration(NamedTuple): Attributes: type (Type[IPipe]): The type of the pipe. - option (PipeConfigurationOption): The configuration option for the pipe. - TODO + option (PipeConfigurationOption): The inclusion option for the pipe. Defaults to + `PipeConfigurationOption.DEFAULT`. + should_ignore_failures (bool): If true, will tell the invoker to continue the pipeline + regardless of failures. Defaults to `false`. ''' type: Type[IPipe] option: PipeConfigurationOption = PipeConfigurationOption.DEFAULT @@ -81,11 +83,6 @@ class EntityExistenceChecker(IPipe): pass -class InputPort: - '''Marks a class as an input port (not an implementation of IPipe). The entry point for all use cases.''' - pass - - class InputPortValidator(IPipe): '''Marks a class as an input port validator pipe. Used to enforce integrity and correctness of input data.''' pass @@ -103,25 +100,28 @@ class PersistenceRuleValidator(IPipe): def required(func): - '''#TODO: docs''' + '''Marks a property on an InputPort as required. Used alongside the `RequiredInputValidator` + pipe, the `required` decorator enforces values to be supplied to use cases.''' def wrapper(self): return func(self) return wrapper class RequiredInputValidator(IPipe): - #TODO: Docs + '''A validation pipe, used to check if any required inputs from the use case's InputPort have + not been given a value. Required inputs are identified via the `required` decorator.''' - async def execute_async(self, input_port: Any, output_port: Any) -> Coroutine[Any, Any, Union[Coroutine, None]]: - properties = [(attr, getattr(input_port.__class__, attr)) for attr in dir(input_port.__class__) + async def execute_async(self, input_port: InputPort, output_port: IValidationOutputPort) -> None: + _Properties = [(attr, getattr(input_port.__class__, attr)) for attr in dir(input_port.__class__) if isinstance(getattr(input_port.__class__, attr), property)] - fails = [] + _MissingInputs = [] - for name, prop in properties: - if prop.__get__(input_port) is None: - fails.append(name) + for _Name, _Property in _Properties: + if _Property.__get__(input_port) is None: + _MissingInputs.append(_Name) - if issubclass(type(output_port), IValidationOutputPort) and fails: - await output_port.present_validation_failure_async(f"Required inputs must have a value: {', '.join(fails)}") + if issubclass(type(output_port), IValidationOutputPort) and _MissingInputs: + await output_port.present_validation_failure_async( + ValidationResult.from_summary(f"Required inputs must have a value: {', '.join(_MissingInputs)}")) self.has_failures = True diff --git a/src/clapy/services.py b/src/clapy/services.py index 0756056..3b49890 100644 --- a/src/clapy/services.py +++ b/src/clapy/services.py @@ -1,8 +1,8 @@ from abc import ABC, abstractmethod from typing import List, Type -from .generics import TInputPort, TOutputPort, TServiceType -from .pipeline import IPipe, PipeConfiguration +from .outputs import IOutputPort +from .pipeline import IPipe, InputPort, PipeConfiguration class IPipelineFactory(ABC): @@ -11,7 +11,7 @@ class IPipelineFactory(ABC): @abstractmethod async def create_pipeline_async( self, - input_port: TInputPort, + input_port: InputPort, pipeline_configuration: List[PipeConfiguration]) -> List[Type[IPipe]]: ''' Summary @@ -36,7 +36,7 @@ class IServiceProvider(ABC): '''A generic interface for getting services from a dependency injection container.''' @abstractmethod - def get_service(self, service: Type[TServiceType]) -> TServiceType: + def get_service(self, service: type) -> object: ''' Summary ------- @@ -55,19 +55,19 @@ def get_service(self, service: Type[TServiceType]) -> TServiceType: class IUseCaseInvoker(ABC): - '''The main engine of Clapy. Handles the invocation of use case pipelines and the execution of resulting actions.''' + '''The main engine of Clapy. Handles the invocation of use case pipelines.''' @abstractmethod async def invoke_usecase_async( self, - input_port: TInputPort, - output_port: TOutputPort, + input_port: InputPort, + output_port: IOutputPort, pipeline_configuration: List[Type[IPipe]]) -> None: ''' Summary ------- Performs the invocation of a use case with the provided input and output ports. Will stop - invocation on receival of a coroutine result, or if the pipeline's pipes are exhausted. + the pipeline if the pipeline's pipes are exhausted, or on pipe failure unless configured to ignore. Parameters ----------