[WIP] Added first changes for the DI

This commit is contained in:
Sven Heidemann 2021-03-23 20:21:24 +01:00
parent 36dfedf918
commit c650e87443
12 changed files with 375 additions and 173 deletions

View File

@ -6,6 +6,7 @@ from cpl.application.application_runtime import ApplicationRuntime
from cpl.application.startup_abc import StartupABC from cpl.application.startup_abc import StartupABC
from cpl.configuration import Configuration from cpl.configuration import Configuration
from cpl.dependency_injection import ServiceProvider from cpl.dependency_injection import ServiceProvider
from cpl.dependency_injection.service_collection import ServiceCollection
class ApplicationBuilder(ApplicationBuilderABC): class ApplicationBuilder(ApplicationBuilderABC):
@ -20,7 +21,7 @@ class ApplicationBuilder(ApplicationBuilderABC):
self._configuration = Configuration() self._configuration = Configuration()
self._runtime = ApplicationRuntime() self._runtime = ApplicationRuntime()
self._services = ServiceProvider(self._configuration, self._runtime) self._services = ServiceCollection(self._configuration, self._runtime)
def use_startup(self, startup: Type[StartupABC]): def use_startup(self, startup: Type[StartupABC]):
""" """
@ -39,4 +40,4 @@ class ApplicationBuilder(ApplicationBuilderABC):
self._startup.configure_configuration() self._startup.configure_configuration()
self._startup.configure_services() self._startup.configure_services()
return self._app(self._configuration, self._runtime, self._services) return self._app(self._configuration, self._runtime, self._services.build_service_provider())

View File

@ -21,7 +21,7 @@ from collections import namedtuple
# imports: # imports:
from .service_abc import ServiceABC from .service_abc import ServiceABC
from .service_provider import ServiceProvider from .service_provider_old import ServiceProvider
from .service_provider_abc import ServiceProviderABC from .service_provider_abc import ServiceProviderABC
VersionInfo = namedtuple('VersionInfo', 'major minor micro') VersionInfo = namedtuple('VersionInfo', 'major minor micro')

View File

@ -0,0 +1,67 @@
from typing import Union, Type, Callable, Optional
from cpl.application.application_runtime_abc import ApplicationRuntimeABC
from cpl.configuration.configuration_abc import ConfigurationABC
from cpl.database.context import DatabaseContextABC
from cpl.dependency_injection.service_factory import ServiceFactory
from cpl.dependency_injection.service_provider_abc import ServiceProviderABC
from cpl.dependency_injection.service_collection_abc import ServiceCollectionABC
from cpl.dependency_injection.service_descriptor import ServiceDescriptor
from cpl.dependency_injection.service_lifetime_enum import ServiceLifetimeEnum
from cpl.dependency_injection.service_provider import ServiceProvider
class ServiceCollection(ServiceCollectionABC):
def __init__(self, config: ConfigurationABC, runtime: ApplicationRuntimeABC):
ServiceCollectionABC.__init__(self)
self._configuration: ConfigurationABC = config
self._runtime: ApplicationRuntimeABC = runtime
self._database_context: Optional[DatabaseContextABC] = None
self._service_descriptors: list[ServiceDescriptor] = []
def _add_descriptor(self, service: Union[type, object], lifetime: ServiceLifetimeEnum):
found = False
for descriptor in self._service_descriptors:
if not isinstance(service, type):
service = type(service)
if descriptor.service_type == service:
found = True
if found:
service_type = service
if not isinstance(service, type):
service_type = type(service)
raise Exception(f'Service of type {service_type} already exists')
self._service_descriptors.append(ServiceDescriptor(service, lifetime))
def add_db_context(self, db_context: Type[DatabaseContextABC]):
pass
def add_singleton(self, service_type: Union[type, object], service: Union[type, object] = None):
if service is not None:
if isinstance(service, type):
service = service()
self._add_descriptor(service, ServiceLifetimeEnum.singleton)
else:
if isinstance(service_type, type):
service_type = service_type()
self._add_descriptor(service_type, ServiceLifetimeEnum.singleton)
def add_scoped(self, service_type: Type, service: Callable = None):
pass
def add_transient(self, service_type: Union[type], service: Union[type] = None):
if service is not None:
self._add_descriptor(service, ServiceLifetimeEnum.transient)
else:
self._add_descriptor(service_type, ServiceLifetimeEnum.transient)
def build_service_provider(self) -> ServiceProviderABC:
return ServiceProvider(ServiceFactory(self._service_descriptors, self._configuration, self._runtime))

View File

@ -0,0 +1,62 @@
from abc import abstractmethod, ABC
from collections import Callable
from typing import Type
from cpl.database.context.database_context_abc import DatabaseContextABC
from cpl.dependency_injection.service_provider_abc import ServiceProviderABC
class ServiceCollectionABC(ABC):
@abstractmethod
def __init__(self):
"""
ABC for service providing
"""
pass
@abstractmethod
def add_db_context(self, db_context: Type[DatabaseContextABC]):
"""
Adds database context
:param db_context:
:return:
"""
pass
@abstractmethod
def add_transient(self, service_type: Type, service: Callable = None):
"""
Adds a service with transient lifetime
:param service_type:
:param service:
:return:
"""
pass
@abstractmethod
def add_scoped(self, service_type: Type, service: Callable = None):
"""
Adds a service with scoped lifetime
:param service_type:
:param service:
:return:
"""
pass
@abstractmethod
def add_singleton(self, service_type: Type, service: Callable = None):
"""
Adds a service with singleton lifetime
:param service_type:
:param service:
:return:
"""
pass
@abstractmethod
def build_service_provider(self) -> ServiceProviderABC:
"""
Creates instance of the service provider
"""
pass

View File

@ -0,0 +1,33 @@
from typing import Union, Optional
from cpl.dependency_injection.service_lifetime_enum import ServiceLifetimeEnum
class ServiceDescriptor:
def __init__(self, implementation: Union[type, Optional[object]], lifetime: ServiceLifetimeEnum):
self._service_type = implementation
self._implementation = implementation
self._lifetime = lifetime
if not isinstance(implementation, type):
self._service_type = type(implementation)
else:
self._implementation = None
@property
def service_type(self) -> type:
return self._service_type
@property
def implementation(self) -> Union[type, Optional[object]]:
return self._implementation
@implementation.setter
def implementation(self, implementation: Union[type, Optional[object]]):
self._implementation = implementation
@property
def lifetime(self) -> ServiceLifetimeEnum:
return self._lifetime

View File

@ -0,0 +1,27 @@
from cpl.application.application_runtime_abc import ApplicationRuntimeABC
from cpl.configuration.configuration_abc import ConfigurationABC
from cpl.dependency_injection.service_descriptor import ServiceDescriptor
from cpl.dependency_injection.service_factory_abc import ServiceFactoryABC
class ServiceFactory(ServiceFactoryABC):
def __init__(self, service_descriptors: list[ServiceDescriptor], config: ConfigurationABC,
runtime: ApplicationRuntimeABC):
ServiceFactoryABC.__init__(self)
self._service_descriptors: list[ServiceDescriptor] = service_descriptors
self._configuration: ConfigurationABC = config
self._runtime: ApplicationRuntimeABC = runtime
@property
def service_descriptors(self) -> list[ServiceDescriptor]:
return self._service_descriptors
@property
def configuration(self) -> ConfigurationABC:
return self._configuration
@property
def runtime(self) -> ApplicationRuntimeABC:
return self._runtime

View File

@ -0,0 +1,23 @@
from abc import ABC, abstractmethod
from cpl.application.application_runtime_abc import ApplicationRuntimeABC
from cpl.configuration.configuration_abc import ConfigurationABC
from cpl.dependency_injection.service_descriptor import ServiceDescriptor
class ServiceFactoryABC(ABC):
@abstractmethod
def __init__(self): pass
@property
@abstractmethod
def service_descriptors(self) -> list[ServiceDescriptor]: pass
@property
@abstractmethod
def configuration(self) -> ConfigurationABC: pass
@property
@abstractmethod
def runtime(self) -> ApplicationRuntimeABC: pass

View File

@ -0,0 +1,8 @@
from enum import Enum
class ServiceLifetimeEnum(Enum):
singleton = 0
scoped = 1 # not supported yet
transient = 2

View File

@ -1,122 +1,37 @@
from collections import Callable from collections import Callable
from inspect import signature, Parameter from typing import Optional
from typing import Type, Optional, Union
from cpl.application.application_runtime_abc import ApplicationRuntimeABC from cpl.dependency_injection import ServiceProviderABC
from cpl.configuration.configuration_abc import ConfigurationABC from cpl.dependency_injection.service_descriptor import ServiceDescriptor
from cpl.configuration.configuration_model_abc import ConfigurationModelABC from cpl.dependency_injection.service_factory_abc import ServiceFactoryABC
from cpl.database.context.database_context_abc import DatabaseContextABC from cpl.dependency_injection.service_lifetime_enum import ServiceLifetimeEnum
from cpl.dependency_injection.service_abc import ServiceABC
from cpl.dependency_injection.service_provider_abc import ServiceProviderABC
from cpl.environment.environment_abc import ApplicationEnvironmentABC
class ServiceProvider(ServiceProviderABC): class ServiceProvider(ServiceProviderABC):
def __init__(self, config: ConfigurationABC, runtime: ApplicationRuntimeABC): def __init__(self, service_factory: ServiceFactoryABC):
"""
Service for service providing
:param runtime:
"""
ServiceProviderABC.__init__(self) ServiceProviderABC.__init__(self)
self._configuration: ConfigurationABC = config
self._runtime: ApplicationRuntimeABC = runtime
self._database_context: Optional[DatabaseContextABC] = None
self._transient_services: dict[Type[ServiceABC], Callable[ServiceABC]] = {} self._service_factory = service_factory
self._scoped_services: dict[Type[ServiceABC], Callable[ServiceABC]] = {}
self._singleton_services: dict[Type[ServiceABC], Callable[ServiceABC], ServiceABC] = {}
def _create_instance(self, service: Union[Callable[ServiceABC], ServiceABC]) -> Callable[ServiceABC]: def _find_service(self, service_type: type) -> [ServiceDescriptor]:
""" for descriptor in self._service_factory.service_descriptors:
Creates an instance of given type if descriptor.service_type == service_type or issubclass(descriptor.service_type, service_type):
:param service: return descriptor
:return:
"""
sig = signature(service.__init__)
params = []
for param in sig.parameters.items():
parameter = param[1]
if parameter.name != 'self' and parameter.annotation != Parameter.empty:
if issubclass(parameter.annotation, ApplicationRuntimeABC):
params.append(self._runtime)
elif issubclass(parameter.annotation, ApplicationEnvironmentABC): return None
params.append(self._configuration.environment)
elif issubclass(parameter.annotation, DatabaseContextABC): def get_service(self, service_type: type) -> Optional[Callable[object]]:
params.append(self._database_context) result = self._find_service(service_type)
elif issubclass(parameter.annotation, ConfigurationModelABC): if result is None:
params.append(self._configuration.get_configuration(parameter.annotation)) return None
elif issubclass(parameter.annotation, ConfigurationABC): if result.implementation is not None:
params.append(self._configuration) return result.implementation
elif issubclass(parameter.annotation, ServiceProviderABC): implementation = result.service_type()
params.append(self) if result.lifetime == ServiceLifetimeEnum.singleton:
result.implementation = implementation
else: return implementation
params.append(self.get_service(parameter.annotation))
return service(*params)
def add_db_context(self, db_context: Type[DatabaseContextABC]):
self._database_context = self._create_instance(db_context)
def get_db_context(self) -> Callable[DatabaseContextABC]:
return self._database_context
def add_transient(self, service_type: Type[ServiceABC], service: Callable[ServiceABC] = None):
if service is None:
self._transient_services[service_type] = service_type
else:
self._transient_services[service_type] = service
def add_scoped(self, service_type: Type[ServiceABC], service: Callable[ServiceABC] = None):
if service is None:
self._scoped_services[service_type] = service_type
else:
self._scoped_services[service_type] = service
def add_singleton(self, service_type: Type[ServiceABC], service: Callable[ServiceABC] = None):
for known_service in self._singleton_services:
if type(known_service) == service_type:
raise Exception(f'Service with type {service_type} already exists')
if service is None:
self._singleton_services[service_type] = self._create_instance(service_type)
else:
self._singleton_services[service_type] = self._create_instance(service)
def get_service(self, instance_type: Type) -> Callable[ServiceABC]:
if issubclass(instance_type, ServiceProviderABC):
return self
for service in self._transient_services:
if service == instance_type and isinstance(self._transient_services[service], type(instance_type)):
return self._create_instance(self._transient_services[service])
for service in self._scoped_services:
if service == instance_type and isinstance(self._scoped_services[service], type(instance_type)):
return self._create_instance(self._scoped_services[service])
for service in self._singleton_services:
if service == instance_type and isinstance(self._singleton_services[service], instance_type):
return self._singleton_services[service]
def remove_service(self, instance_type: Type[ServiceABC]):
for service in self._transient_services:
if service == instance_type and isinstance(self._transient_services[service], type(instance_type)):
del self._transient_services[service]
return
for service in self._scoped_services:
if service == instance_type and isinstance(self._scoped_services[service], type(instance_type)):
del self._scoped_services[service]
return
for service in self._singleton_services:
if service == instance_type and isinstance(self._singleton_services[service], instance_type):
del self._singleton_services[service]
return

View File

@ -2,7 +2,6 @@ from abc import abstractmethod, ABC
from collections import Callable from collections import Callable
from typing import Type from typing import Type
from cpl.database.context.database_context_abc import DatabaseContextABC
from cpl.dependency_injection.service_abc import ServiceABC from cpl.dependency_injection.service_abc import ServiceABC
@ -15,53 +14,6 @@ class ServiceProviderABC(ABC):
""" """
pass pass
@abstractmethod
def add_db_context(self, db_context: Type[DatabaseContextABC]):
"""
Adds database context
:param db_context:
:return:
"""
pass
@abstractmethod
def get_db_context(self) -> Callable[DatabaseContextABC]:
""""
Returns database context
:return Callable[DatabaseContextABC]:
"""
pass
@abstractmethod
def add_transient(self, service_type: Type, service: Callable = None):
"""
Adds a service with transient lifetime
:param service_type:
:param service:
:return:
"""
pass
@abstractmethod
def add_scoped(self, service_type: Type, service: Callable = None):
"""
Adds a service with scoped lifetime
:param service_type:
:param service:
:return:
"""
pass
@abstractmethod
def add_singleton(self, service_type: Type, service: Callable = None):
"""
Adds a service with singleton lifetime
:param service_type:
:param service:
:return:
"""
pass
@abstractmethod @abstractmethod
def get_service(self, instance_type: Type) -> Callable[ServiceABC]: def get_service(self, instance_type: Type) -> Callable[ServiceABC]:
""" """
@ -70,12 +22,3 @@ class ServiceProviderABC(ABC):
:return: :return:
""" """
pass pass
@abstractmethod
def remove_service(self, instance_type: type):
"""
Removes service
:param instance_type:
:return:
"""
pass

View File

@ -0,0 +1,122 @@
from collections import Callable
from inspect import signature, Parameter
from typing import Type, Optional, Union
from cpl.application.application_runtime_abc import ApplicationRuntimeABC
from cpl.configuration.configuration_abc import ConfigurationABC
from cpl.configuration.configuration_model_abc import ConfigurationModelABC
from cpl.database.context.database_context_abc import DatabaseContextABC
from cpl.dependency_injection.service_abc import ServiceABC
from cpl.dependency_injection.service_provider_abc import ServiceProviderABC
from cpl.environment.environment_abc import ApplicationEnvironmentABC
class ServiceProvider(ServiceProviderABC):
def __init__(self, config: ConfigurationABC, runtime: ApplicationRuntimeABC):
"""
Service for service providing
:param runtime:
"""
ServiceProviderABC.__init__(self)
self._configuration: ConfigurationABC = config
self._runtime: ApplicationRuntimeABC = runtime
self._database_context: Optional[DatabaseContextABC] = None
self._transient_services: dict[Type[ServiceABC], Callable[ServiceABC]] = {}
self._scoped_services: dict[Type[ServiceABC], Callable[ServiceABC]] = {}
self._singleton_services: dict[Type[ServiceABC], Callable[ServiceABC], ServiceABC] = {}
def _create_instance(self, service: Union[Callable[ServiceABC], ServiceABC]) -> Callable[ServiceABC]:
"""
Creates an instance of given type
:param service:
:return:
"""
sig = signature(service.__init__)
params = []
for param in sig.parameters.items():
parameter = param[1]
if parameter.name != 'self' and parameter.annotation != Parameter.empty:
if issubclass(parameter.annotation, ApplicationRuntimeABC):
params.append(self._runtime)
elif issubclass(parameter.annotation, ApplicationEnvironmentABC):
params.append(self._configuration.environment)
elif issubclass(parameter.annotation, DatabaseContextABC):
params.append(self._database_context)
elif issubclass(parameter.annotation, ConfigurationModelABC):
params.append(self._configuration.get_configuration(parameter.annotation))
elif issubclass(parameter.annotation, ConfigurationABC):
params.append(self._configuration)
elif issubclass(parameter.annotation, ServiceProviderABC):
params.append(self)
else:
params.append(self.get_service(parameter.annotation))
return service(*params)
def add_db_context(self, db_context: Type[DatabaseContextABC]):
self._database_context = self._create_instance(db_context)
def get_db_context(self) -> Callable[DatabaseContextABC]:
return self._database_context
def add_transient(self, service_type: Type[ServiceABC], service: Callable[ServiceABC] = None):
if service is None:
self._transient_services[service_type] = service_type
else:
self._transient_services[service_type] = service
def add_scoped(self, service_type: Type[ServiceABC], service: Callable[ServiceABC] = None):
if service is None:
self._scoped_services[service_type] = service_type
else:
self._scoped_services[service_type] = service
def add_singleton(self, service_type: Type[ServiceABC], service: Callable[ServiceABC] = None):
for known_service in self._singleton_services:
if type(known_service) == service_type:
raise Exception(f'Service with type {service_type} already exists')
if service is None:
self._singleton_services[service_type] = self._create_instance(service_type)
else:
self._singleton_services[service_type] = self._create_instance(service)
def get_service(self, instance_type: Type) -> Callable[ServiceABC]:
if issubclass(instance_type, ServiceProviderABC):
return self
for service in self._transient_services:
if service == instance_type and isinstance(self._transient_services[service], type(instance_type)):
return self._create_instance(self._transient_services[service])
for service in self._scoped_services:
if service == instance_type and isinstance(self._scoped_services[service], type(instance_type)):
return self._create_instance(self._scoped_services[service])
for service in self._singleton_services:
if service == instance_type and isinstance(self._singleton_services[service], instance_type):
return self._singleton_services[service]
def remove_service(self, instance_type: Type[ServiceABC]):
for service in self._transient_services:
if service == instance_type and isinstance(self._transient_services[service], type(instance_type)):
del self._transient_services[service]
return
for service in self._scoped_services:
if service == instance_type and isinstance(self._scoped_services[service], type(instance_type)):
del self._scoped_services[service]
return
for service in self._singleton_services:
if service == instance_type and isinstance(self._singleton_services[service], instance_type):
del self._singleton_services[service]
return

View File

@ -3,6 +3,7 @@ from cpl.application.startup_abc import StartupABC
from cpl.configuration.configuration_abc import ConfigurationABC from cpl.configuration.configuration_abc import ConfigurationABC
from cpl.database.context.database_context import DatabaseContext from cpl.database.context.database_context import DatabaseContext
from cpl.database.database_settings import DatabaseSettings from cpl.database.database_settings import DatabaseSettings
from cpl.dependency_injection.service_collection_abc import ServiceCollectionABC
from cpl.dependency_injection.service_provider_abc import ServiceProviderABC from cpl.dependency_injection.service_provider_abc import ServiceProviderABC
from cpl.logging.logger_service import Logger from cpl.logging.logger_service import Logger
from cpl.logging.logger_abc import LoggerABC from cpl.logging.logger_abc import LoggerABC
@ -13,12 +14,13 @@ from cpl.utils.credential_manager import CredentialManager
class Startup(StartupABC): class Startup(StartupABC):
def __init__(self, config: ConfigurationABC, runtime: ApplicationRuntimeABC, services: ServiceProviderABC): def __init__(self, config: ConfigurationABC, runtime: ApplicationRuntimeABC, services: ServiceCollectionABC):
StartupABC.__init__(self) StartupABC.__init__(self)
self._configuration = config self._configuration = config
self._application_runtime = runtime self._application_runtime = runtime
self._services = services self._services = services
print(self._services)
def configure_configuration(self) -> ConfigurationABC: def configure_configuration(self) -> ConfigurationABC:
self._configuration.add_environment_variables('PYTHON_') self._configuration.add_environment_variables('PYTHON_')
@ -32,12 +34,11 @@ class Startup(StartupABC):
def configure_services(self) -> ServiceProviderABC: def configure_services(self) -> ServiceProviderABC:
# Create and connect to database # Create and connect to database
db_settings: DatabaseSettings = self._configuration.get_configuration(DatabaseSettings) # db_settings: DatabaseSettings = self._configuration.get_configuration(DatabaseSettings)
self._services.add_db_context(DatabaseContext) # self._services.add_db_context(DatabaseContext)
db: DatabaseContext = self._services.get_db_context() # db.connect(CredentialManager.build_string(db_settings.connection_string, db_settings.credentials))
db.connect(CredentialManager.build_string(db_settings.connection_string, db_settings.credentials))
self._services.add_singleton(LoggerABC, Logger) self._services.add_singleton(LoggerABC, Logger)
self._services.add_singleton(EMailClientABC, EMailClient) self._services.add_singleton(EMailClientABC, EMailClient)
return self._services return self._services.build_service_provider()