WIP: dev into master #184

Draft
edraft wants to merge 121 commits from dev into master
15 changed files with 112 additions and 103 deletions
Showing only changes of commit 56a16cbeba - Show all commits

View File

@@ -3,6 +3,7 @@ from starlette.responses import JSONResponse
from cpl.api.api_module import ApiModule from cpl.api.api_module import ApiModule
from cpl.api.application.web_app import WebApp from cpl.api.application.web_app import WebApp
from cpl.application.application_builder import ApplicationBuilder from cpl.application.application_builder import ApplicationBuilder
from cpl.auth import AuthModule
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.auth.schema import AuthUser, Role from cpl.auth.schema import AuthUser, Role
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
@@ -39,7 +40,13 @@ def main():
app.with_authentication() app.with_authentication()
app.with_authorization() app.with_authorization()
app.with_route(path="/route1", fn=lambda r: JSONResponse("route1"), method="GET", authentication=True, permissions=[Permissions.administrator]) app.with_route(
path="/route1",
fn=lambda r: JSONResponse("route1"),
method="GET",
authentication=True,
permissions=[Permissions.administrator],
)
app.with_routes_directory("routes") app.with_routes_directory("routes")
provider = builder.service_provider provider = builder.service_provider

View File

@@ -1,22 +1,19 @@
from cpl.auth.auth_module import AuthModule from cpl.auth.auth_module import AuthModule
from cpl.auth.permission.permission_module import PermissionsModule from cpl.auth.permission.permission_module import PermissionsModule
from cpl.database.database_module import DatabaseModule from cpl.database.database_module import DatabaseModule
from cpl.dependency.module import Module, TModule from cpl.dependency.module import Module
from cpl.dependency.service_collection import ServiceCollection
class ApiModule(Module): class ApiModule(Module):
dependencies = []
@staticmethod @staticmethod
def dependencies() -> list[TModule]: def register(collection: ServiceCollection):
return [AuthModule, DatabaseModule, PermissionsModule]
@staticmethod
def register(collection: "ServiceCollection"):
from cpl.api.registry.policy import PolicyRegistry from cpl.api.registry.policy import PolicyRegistry
from cpl.api.registry.route import RouteRegistry from cpl.api.registry.route import RouteRegistry
collection.add_module(DatabaseModule) collection.add_module(DatabaseModule)
collection.add_module(AuthModule) collection.add_module(AuthModule)
collection.add_module(PermissionsModule) collection.add_module(PermissionsModule)

View File

@@ -2,15 +2,15 @@ import os
from cpl.database.database_module import DatabaseModule from cpl.database.database_module import DatabaseModule
from cpl.database.model.server_type import ServerType, ServerTypes from cpl.database.model.server_type import ServerType, ServerTypes
from cpl.database.mysql.mysql_module import MySQLModule
from cpl.database.postgres.postgres_module import PostgresModule
from cpl.database.service.migration_service import MigrationService from cpl.database.service.migration_service import MigrationService
from cpl.dependency.module import Module, TModule from cpl.dependency.module import Module
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
class AuthModule(Module): class AuthModule(Module):
@staticmethod dependencies = [DatabaseModule, (MySQLModule, PostgresModule)]
def dependencies() -> list[TModule]:
return [DatabaseModule]
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):

View File

@@ -3,16 +3,13 @@ from cpl.auth.permission.permission_seeder import PermissionSeeder
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.auth.permission.permissions_registry import PermissionsRegistry
from cpl.database.abc.data_seeder_abc import DataSeederABC from cpl.database.abc.data_seeder_abc import DataSeederABC
from cpl.dependency.module import Module, TModule from cpl.database.database_module import DatabaseModule
from cpl.dependency.module import Module
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
class PermissionsModule(Module): class PermissionsModule(Module):
@staticmethod dependencies = [DatabaseModule, AuthModule]
def dependencies() -> list[TModule]:
from cpl.database.database_module import DatabaseModule
return [DatabaseModule, AuthModule]
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):

View File

@@ -16,7 +16,7 @@ def dependency_error(src: str, package_name: str, e: ImportError = None) -> None
def module_dependency_error(src: str, module: str, e: ImportError = None) -> None: def module_dependency_error(src: str, module: str, e: ImportError = None) -> None:
Console.error(f"'{module}' is required to use feature: {src}. Please initialize it with `add_module({module})`.") Console.error(f"'{module}' is required by '{src}'. Please initialize it with `add_module({module})`.")
tb = traceback.format_exc() tb = traceback.format_exc()
if not tb.startswith("NoneType: None"): if not tb.startswith("NoneType: None"):
Console.write_line("->", tb) Console.write_line("->", tb)

View File

@@ -1,16 +1,11 @@
from cpl.core.errors import module_dependency_error from cpl.database.mysql.mysql_module import MySQLModule
from cpl.database.model.server_type import ServerType from cpl.database.postgres.postgres_module import PostgresModule
from cpl.dependency.module import Module, TModule from cpl.dependency.module import Module
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
class DatabaseModule(Module): class DatabaseModule(Module):
@staticmethod dependencies = [(MySQLModule, PostgresModule)]
def dependencies() -> list[TModule]:
if not ServerType.has_server_type:
module_dependency_error(__name__, "MySQLModule or PostgresModule")
return []
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):

View File

@@ -1,13 +1,11 @@
from cpl.core.configuration.configuration import Configuration from cpl.core.configuration.configuration import Configuration
from cpl.database.model.server_type import ServerTypes, ServerType from cpl.database.model.server_type import ServerTypes, ServerType
from cpl.dependency.module import Module, TModule from cpl.dependency.module import Module
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
class MySQLModule(Module): class MySQLModule(Module):
@staticmethod dependencies = []
def dependencies() -> list[TModule]:
return []
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):

View File

@@ -1,14 +1,11 @@
from cpl.core.configuration.configuration import Configuration from cpl.core.configuration.configuration import Configuration
from cpl.database.database_module import DatabaseModule
from cpl.database.model.server_type import ServerTypes, ServerType from cpl.database.model.server_type import ServerTypes, ServerType
from cpl.dependency.module import Module, TModule from cpl.dependency.module import Module
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
class PostgresModule(Module): class PostgresModule(Module):
@staticmethod dependencies = []
def dependencies() -> list[TModule]:
return [DatabaseModule]
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):

View File

@@ -1,14 +1,22 @@
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from typing import Type from inspect import isclass
TModule = Type["Module"]
class Module(ABC): class Module(ABC):
__REQUIRED_VARS = ["dependencies"]
@staticmethod def __init_subclass__(cls):
@abstractmethod super().__init_subclass__()
def dependencies() -> list[TModule]: ... for var in cls.__REQUIRED_VARS:
if not hasattr(cls, var):
raise TypeError(f"Can't instantiate abstract class {cls.__name__} without {var} attribute")
if not isinstance(getattr(cls, var), list):
raise TypeError(f"Can't instantiate abstract class {cls.__name__} with non-list {var} attribute")
for dep in getattr(cls, var):
if not isinstance(dep, (list, tuple)) and not isclass(dep):
raise TypeError(f"Can't instantiate abstract class {cls.__name__} with invalid dependency {dep}")
@staticmethod @staticmethod
@abstractmethod @abstractmethod

View File

@@ -1,6 +1,7 @@
from inspect import isclass from inspect import isclass
from typing import Union, Type, Callable, Self from typing import Union, Callable, Self, Type
from cpl.core.errors import module_dependency_error
from cpl.core.log.logger_abc import LoggerABC from cpl.core.log.logger_abc import LoggerABC
from cpl.core.typing import T, Service from cpl.core.typing import T, Service
from cpl.core.utils.cache import Cache from cpl.core.utils.cache import Cache
@@ -9,6 +10,7 @@ from cpl.dependency.module import Module
from cpl.dependency.service_descriptor import ServiceDescriptor from cpl.dependency.service_descriptor import ServiceDescriptor
from cpl.dependency.service_lifetime import ServiceLifetimeEnum from cpl.dependency.service_lifetime import ServiceLifetimeEnum
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
from cpl.dependency.typing import TModule, TService, TStartupTask
class ServiceCollection: class ServiceCollection:
@@ -16,11 +18,6 @@ class ServiceCollection:
_modules: dict[str, Callable] = {} _modules: dict[str, Callable] = {}
@classmethod
def with_module(cls, func: Callable, name: str = None) -> type[Self]:
# cls._modules[func.__name__ if name is None else name] = func
return cls
def __init__(self): def __init__(self):
self._service_descriptors: list[ServiceDescriptor] = [] self._service_descriptors: list[ServiceDescriptor] = []
self._loaded_modules: set[str] = set() self._loaded_modules: set[str] = set()
@@ -29,6 +26,47 @@ class ServiceCollection:
def loaded_modules(self) -> set[str]: def loaded_modules(self) -> set[str]:
return self._loaded_modules return self._loaded_modules
def _check_dependency(self, module: TModule, dependency: TModule | TService, optional: bool = False) -> bool:
if not issubclass(dependency, Module):
found_services = [
x
for x in self._service_descriptors
if x.service_type == dependency or x.base_type == dependency or isinstance(x.implementation, dependency)
]
if len(found_services) > 0:
return True
if optional:
return False
module_dependency_error(module.__name__, dependency.__name__)
if dependency.__name__ not in self._loaded_modules:
if optional:
return False
module_dependency_error(module.__name__, dependency.__name__)
return True
def _check_dependencies(self, module: TModule):
dependencies: list[TModule | Type] = getattr(module, "dependencies", [])
for dependency in dependencies:
if isinstance(dependency, (list, tuple)):
deps_exists = [self._check_dependency(module, dep, optional=True) for dep in dependency]
if not any(deps_exists):
if len(dependency) > 1:
names = ", ".join([dep.__name__ for dep in dependency[:-1]]) + f" or {dependency[-1].__name__}"
else:
names = dependency[0].__name__
module_dependency_error(module.__name__, names)
continue
self._check_dependency(module, dependency)
def _add_descriptor(self, service: Union[type, object], lifetime: ServiceLifetimeEnum, base_type: Callable = None): def _add_descriptor(self, service: Union[type, object], lifetime: ServiceLifetimeEnum, base_type: Callable = None):
found = False found = False
for descriptor in self._service_descriptors: for descriptor in self._service_descriptors:
@@ -44,7 +82,9 @@ class ServiceCollection:
self._service_descriptors.append(ServiceDescriptor(service, lifetime, base_type)) self._service_descriptors.append(ServiceDescriptor(service, lifetime, base_type))
def _add_descriptor_by_lifetime(self, service_type: Type, lifetime: ServiceLifetimeEnum, service: Callable = None): def _add_descriptor_by_lifetime(
self, service_type: TService | T, lifetime: ServiceLifetimeEnum, service: Callable = None
):
if service is not None: if service is not None:
self._add_descriptor(service, lifetime, service_type) self._add_descriptor(service, lifetime, service_type)
else: else:
@@ -52,19 +92,19 @@ class ServiceCollection:
return self return self
def add_singleton(self, service_type: T, service: Service = None) -> Self: def add_singleton(self, service_type: TService | T, service: Service = None) -> Self:
self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.singleton, service) self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.singleton, service)
return self return self
def add_scoped(self, service_type: T, service: Service = None) -> Self: def add_scoped(self, service_type: TService | T, service: Service = None) -> Self:
self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.scoped, service) self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.scoped, service)
return self return self
def add_transient(self, service_type: T, service: Service = None) -> Self: def add_transient(self, service_type: TService | T, service: Service = None) -> Self:
self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.transient, service) self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.transient, service)
return self return self
def add_startup_task(self, task: Type[StartupTask]) -> Self: def add_startup_task(self, task: TStartupTask) -> Self:
self.add_singleton(StartupTask, task) self.add_singleton(StartupTask, task)
return self return self
@@ -76,17 +116,15 @@ class ServiceCollection:
sp = ServiceProvider(self._service_descriptors) sp = ServiceProvider(self._service_descriptors)
return sp return sp
def add_module(self, module: Type[Module]) -> Self: def add_module(self, module: TModule) -> Self:
assert isclass(module), "Module must be a Module" assert isclass(module), "Module must be a Module"
assert issubclass(module, Module), f"Module must be subclass of {Module.__name__}" assert issubclass(module, Module), f"Module must be subclass of {Module.__name__}"
name = module.__name__ name = module.__name__
if module in self._modules: if module in self._modules:
raise ValueError(f"Module {module} not found") raise ValueError(f"Module {name} is already registered")
for dependency in module.dependencies(): self._check_dependencies(module)
if dependency.__name__ not in self._loaded_modules:
self.add_module(dependency)
module().register(self) module().register(self)
@@ -114,6 +152,6 @@ class ServiceCollection:
self.add_transient(wrapper) self.add_transient(wrapper)
return self return self
def add_cache(self, t: Type[T]): def add_cache(self, t: TService):
self._service_descriptors.append(ServiceDescriptor(Cache(t=t), ServiceLifetimeEnum.singleton, Cache[t])) self._service_descriptors.append(ServiceDescriptor(Cache(t=t), ServiceLifetimeEnum.singleton, Cache[t]))
return self return self

View File

@@ -0,0 +1,9 @@
from typing import Type
from cpl.core.typing import T
from cpl.dependency.hosted import StartupTask
from cpl.dependency.module import Module
TModule = Type[Module]
TService = Type[T]
TStartupTask = Type[StartupTask]

View File

@@ -1,21 +1,6 @@
from cpl.dependency import ServiceCollection as _ServiceCollection
from .abc.email_client_abc import EMailClientABC from .abc.email_client_abc import EMailClientABC
from .email_client import EMailClient from .email_client import EMailClient
from .email_client_settings import EMailClientSettings from .email_client_settings import EMailClientSettings
from .email_model import EMail from .email_model import EMail
from .mail_module import MailModule
from .logger import MailLogger from .logger import MailLogger
from .mail_module import MailModule
def add_mail(collection: _ServiceCollection):
from cpl.core.console import Console
from cpl.core.log import LoggerABC
try:
collection.add_singleton(EMailClientABC, EMailClient)
collection.add_transient(LoggerABC, MailLogger)
except ImportError as e:
Console.error("cpl-translation is not installed", str(e))
_ServiceCollection.with_module(add_mail, __name__)

View File

@@ -1,11 +1,9 @@
from cpl.dependency import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
from cpl.dependency.module import Module, TModule from cpl.dependency.module import Module, TModule
class MailModule(Module): class MailModule(Module):
@staticmethod dependencies = []
def dependencies() -> list[TModule]:
return []
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):

View File

@@ -1,23 +1,5 @@
from cpl.dependency import ServiceCollection as _ServiceCollection
from .translate_pipe import TranslatePipe from .translate_pipe import TranslatePipe
from .translation_module import TranslationModule from .translation_module import TranslationModule
from .translation_service import TranslationService from .translation_service import TranslationService
from .translation_service_abc import TranslationServiceABC from .translation_service_abc import TranslationServiceABC
from .translation_settings import TranslationSettings from .translation_settings import TranslationSettings
def add_translation(collection: _ServiceCollection):
from cpl.core.console import Console
from cpl.core.pipes import PipeABC
from cpl.translation.translate_pipe import TranslatePipe
from cpl.translation.translation_service import TranslationService
from cpl.translation.translation_service_abc import TranslationServiceABC
try:
collection.add_singleton(TranslationServiceABC, TranslationService)
collection.add_transient(PipeABC, TranslatePipe)
except ImportError as e:
Console.error("cpl-translation is not installed", str(e))
_ServiceCollection.with_module(add_translation, __name__)

View File

@@ -1,12 +1,10 @@
from cpl.dependency import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
from cpl.dependency.module import Module, TModule from cpl.dependency.module import Module
from cpl.translation.translation_service_abc import TranslationServiceABC from cpl.translation.translation_service_abc import TranslationServiceABC
class TranslationModule(Module): class TranslationModule(Module):
@staticmethod dependencies = []
def dependencies() -> list[TModule]:
return []
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):