diff --git a/example/api/src/main.py b/example/api/src/main.py index 94ba5901..4732035b 100644 --- a/example/api/src/main.py +++ b/example/api/src/main.py @@ -35,7 +35,6 @@ def main(): app = builder.build() app.with_logging() - app.with_database() app.with_authentication() app.with_authorization() diff --git a/example/database/src/application.py b/example/database/src/application.py index 39112dbc..465a3fee 100644 --- a/example/database/src/application.py +++ b/example/database/src/application.py @@ -4,6 +4,7 @@ from cpl.core.console import Console from cpl.core.environment import Environment from cpl.core.log import LoggerABC from cpl.dependency import ServiceProvider +from cpl.dependency.typing import Modules from model.city import City from model.city_dao import CityDao from model.user import User @@ -11,8 +12,8 @@ from model.user_dao import UserDao class Application(ApplicationABC): - def __init__(self, services: ServiceProvider): - ApplicationABC.__init__(self, services) + def __init__(self, services: ServiceProvider, modules: Modules): + ApplicationABC.__init__(self, services, modules) self._logger = services.get_service(LoggerABC) diff --git a/example/database/src/main_simplified.py b/example/database/src/main_simplified.py index 8b1568ba..d7c700d2 100644 --- a/example/database/src/main_simplified.py +++ b/example/database/src/main_simplified.py @@ -3,6 +3,7 @@ from cpl.application import ApplicationBuilder from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.core.console import Console from cpl.core.log import LogLevel +from cpl.database import DatabaseModule from custom_permissions import CustomPermissions from startup import Startup @@ -10,13 +11,12 @@ from startup import Startup def main(): builder = ApplicationBuilder(Application).with_startup(Startup) builder.services.add_logging() - app = builder.build() app.with_logging(LogLevel.trace) app.with_permissions(CustomPermissions) app.with_migrations("./scripts") - app.with_seeders() + # app.with_seeders() Console.write_line(CustomPermissions.test.value in PermissionsRegistry.get()) app.run() diff --git a/example/database/src/startup.py b/example/database/src/startup.py index 31fd4c54..7af90ec0 100644 --- a/example/database/src/startup.py +++ b/example/database/src/startup.py @@ -6,7 +6,7 @@ from cpl.auth.permission.permission_module import PermissionsModule from cpl.core.configuration import Configuration from cpl.core.environment import Environment from cpl.core.log import Logger, LoggerABC -from cpl.database import mysql +from cpl.database import mysql, DatabaseModule from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.mysql.mysql_module import MySQLModule from cpl.dependency import ServiceCollection @@ -25,6 +25,7 @@ class Startup(StartupABC): @staticmethod async def configure_services(services: ServiceCollection): services.add_module(MySQLModule) + services.add_module(DatabaseModule) services.add_module(AuthModule) services.add_module(PermissionsModule) diff --git a/src/cpl-api/cpl/api/api_module.py b/src/cpl-api/cpl/api/api_module.py index 5c33410b..79d7640c 100644 --- a/src/cpl-api/cpl/api/api_module.py +++ b/src/cpl-api/cpl/api/api_module.py @@ -1,21 +1,22 @@ +from cpl.api import ApiSettings +from cpl.api.registry.policy import PolicyRegistry +from cpl.api.registry.route import RouteRegistry from cpl.auth.auth_module import AuthModule from cpl.auth.permission.permission_module import PermissionsModule from cpl.database.database_module import DatabaseModule -from cpl.dependency.module import Module -from cpl.dependency.service_collection import ServiceCollection +from cpl.dependency import ServiceCollection +from cpl.dependency.module.module import Module class ApiModule(Module): - dependencies = [] + config = [ApiSettings] + singleton = [ + PolicyRegistry, + RouteRegistry, + ] @staticmethod def register(collection: ServiceCollection): - from cpl.api.registry.policy import PolicyRegistry - from cpl.api.registry.route import RouteRegistry - collection.add_module(DatabaseModule) collection.add_module(AuthModule) collection.add_module(PermissionsModule) - - collection.add_singleton(PolicyRegistry) - collection.add_singleton(RouteRegistry) diff --git a/src/cpl-api/cpl/api/application/web_app.py b/src/cpl-api/cpl/api/application/web_app.py index cec6a034..476e54d2 100644 --- a/src/cpl-api/cpl/api/application/web_app.py +++ b/src/cpl-api/cpl/api/application/web_app.py @@ -10,7 +10,7 @@ from starlette.requests import Request from starlette.responses import JSONResponse from starlette.types import ExceptionHandler -from cpl import api, auth +from cpl.api.api_module import ApiModule from cpl.api.error import APIError from cpl.api.logger import APILogger from cpl.api.middleware.authentication import AuthenticationMiddleware @@ -25,20 +25,20 @@ from cpl.api.registry.route import RouteRegistry from cpl.api.router import Router from cpl.api.settings import ApiSettings from cpl.api.typing import HTTPMethods, PartialMiddleware, PolicyResolver -from cpl.api.api_module import ApiModule from cpl.application.abc.application_abc import ApplicationABC from cpl.auth.auth_module import AuthModule from cpl.auth.permission.permission_module import PermissionsModule -from cpl.core.configuration import Configuration +from cpl.core.configuration.configuration import Configuration from cpl.dependency.inject import inject from cpl.dependency.service_provider import ServiceProvider +from cpl.dependency.typing import Modules PolicyInput = Union[dict[str, PolicyResolver], Policy] class WebApp(ApplicationABC): - def __init__(self, services: ServiceProvider): - super().__init__(services, [AuthModule, PermissionsModule, ApiModule]) + def __init__(self, services: ServiceProvider, modules: Modules): + super().__init__(services, modules, [AuthModule, PermissionsModule, ApiModule]) self._app: Starlette | None = None self._logger = services.get_service(APILogger) @@ -78,11 +78,6 @@ class WebApp(ApplicationABC): self._logger.debug(f"Allowed origins: {origins}") return origins.split(",") - def with_database(self) -> Self: - self.with_migrations() - self.with_seeders() - return self - def with_app(self, app: Starlette) -> Self: assert app is not None, "app must not be None" assert isinstance(app, Starlette), "app must be an instance of Starlette" diff --git a/src/cpl-application/cpl/application/abc/application_abc.py b/src/cpl-application/cpl/application/abc/application_abc.py index 1395402a..2ed5c342 100644 --- a/src/cpl-application/cpl/application/abc/application_abc.py +++ b/src/cpl-application/cpl/application/abc/application_abc.py @@ -2,10 +2,12 @@ from abc import ABC, abstractmethod from typing import Callable, Self from cpl.application.host import Host +from cpl.core.errors import module_dependency_error from cpl.core.log.log_level import LogLevel from cpl.core.log.log_settings import LogSettings from cpl.core.log.logger_abc import LoggerABC from cpl.dependency.service_provider import ServiceProvider +from cpl.dependency.typing import TModule def __not_implemented__(package: str, func: Callable): @@ -20,17 +22,6 @@ class ApplicationABC(ABC): Contains instances of prepared objects """ - @abstractmethod - def __init__(self, services: ServiceProvider, required_modules: list[str | object] = None): - self._services = services - self._required_modules = ( - [x.__name__ if not isinstance(x, str) else x for x in required_modules] if required_modules else [] - ) - - @property - def required_modules(self) -> list[str]: - return self._required_modules - @classmethod def extend(cls, name: str | Callable, func: Callable[[Self], Self]): r"""Extend the Application with a custom method @@ -47,6 +38,30 @@ class ApplicationABC(ABC): setattr(cls, name, func) return cls + @abstractmethod + def __init__( + self, services: ServiceProvider, loaded_modules: set[TModule], required_modules: list[str | object] = None + ): + self._services = services + self._modules = loaded_modules + self._required_modules = ( + [x.__name__ if not isinstance(x, str) else x for x in required_modules] if required_modules else [] + ) + + def validate_app_required_modules(self): + modules_names = {x.__name__ for x in self._modules} + for module in self._required_modules: + if module in modules_names: + continue + + module_dependency_error( + type(self).__name__, + module.__name__, + ImportError( + f"Required module '{module}' for application '{self.__class__.__name__}' is not loaded. Load using 'add_module({module})' method." + ), + ) + def with_logging(self, level: LogLevel = None): if level is None: from cpl.core.configuration.configuration import Configuration @@ -57,14 +72,21 @@ class ApplicationABC(ABC): logger = self._services.get_service(LoggerABC) logger.set_level(level) - def with_permissions(self, *args, **kwargs): - __not_implemented__("cpl-auth", self.with_permissions) + def with_permissions(self, *args): + try: + from cpl.auth import AuthModule - def with_migrations(self, *args, **kwargs): - __not_implemented__("cpl-database", self.with_migrations) + AuthModule.with_permissions(*args) + except ImportError: + __not_implemented__("cpl-auth", self.with_permissions) - def with_seeders(self, *args, **kwargs): - __not_implemented__("cpl-database", self.with_seeders) + def with_migrations(self, *args): + try: + from cpl.database.database_module import DatabaseModule + + DatabaseModule.with_migrations(self._services, *args) + except ImportError: + __not_implemented__("cpl-database", self.with_migrations) def with_extension(self, func: Callable[[Self, ...], None], *args, **kwargs): r"""Extend the Application with a custom method @@ -84,6 +106,11 @@ class ApplicationABC(ABC): Called by custom Application.main """ try: + for module in self._modules: + if not hasattr(module, "configure") and not callable(getattr(module, "configure")): + continue + module.configure(self._services) + Host.run_app(self.main) except KeyboardInterrupt: pass diff --git a/src/cpl-application/cpl/application/application_builder.py b/src/cpl-application/cpl/application/application_builder.py index 073c0ae2..7abefddc 100644 --- a/src/cpl-application/cpl/application/application_builder.py +++ b/src/cpl-application/cpl/application/application_builder.py @@ -43,19 +43,6 @@ class ApplicationBuilder(Generic[TApp]): return provider - def validate_app_required_modules(self, app: ApplicationABC): - for module in app.required_modules: - if module in self._services.loaded_modules: - continue - - dependency_error( - type(app).__name__, - module, - ImportError( - f"Required module '{module}' for application '{app.__class__.__name__}' is not loaded. Load using 'add_module({module})' method." - ), - ) - def with_startup(self, startup: Type[StartupABC]) -> "ApplicationBuilder": self._startup = startup return self @@ -84,6 +71,6 @@ class ApplicationBuilder(Generic[TApp]): Host.run(extension.run, self.service_provider) use_root_provider(self._services.build()) - app = self._app(self.service_provider) - self.validate_app_required_modules(app) + app = self._app(self.service_provider, self._services.loaded_modules) + app.validate_app_required_modules() return app diff --git a/src/cpl-auth/cpl/auth/__init__.py b/src/cpl-auth/cpl/auth/__init__.py index 0b491065..a0f3854a 100644 --- a/src/cpl-auth/cpl/auth/__init__.py +++ b/src/cpl-auth/cpl/auth/__init__.py @@ -1,21 +1,6 @@ -from enum import Enum -from typing import Type - -from cpl.application.abc import ApplicationABC as _ApplicationABC from cpl.auth import permission as _permission from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin as _KeycloakAdmin from cpl.auth.keycloak.keycloak_client import KeycloakClient as _KeycloakClient +from .auth_module import AuthModule from .keycloak_settings import KeycloakSettings from .logger import AuthLogger -from .auth_module import AuthModule - - -def _with_permissions(self: _ApplicationABC, *permissions: Type[Enum]) -> _ApplicationABC: - from cpl.auth.permission.permissions_registry import PermissionsRegistry - - for perm in permissions: - PermissionsRegistry.with_enum(perm) - return self - - -_ApplicationABC.extend(_ApplicationABC.with_permissions, _with_permissions) diff --git a/src/cpl-auth/cpl/auth/auth_module.py b/src/cpl-auth/cpl/auth/auth_module.py index e07d8c3d..ea2b8582 100644 --- a/src/cpl-auth/cpl/auth/auth_module.py +++ b/src/cpl-auth/cpl/auth/auth_module.py @@ -1,45 +1,56 @@ import os +from enum import Enum +from typing import Type +from cpl.auth.keycloak_settings import KeycloakSettings from cpl.database.database_module import DatabaseModule 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.dependency.module import Module -from cpl.dependency.service_collection import ServiceCollection +from cpl.dependency.module.module import Module +from cpl.dependency.service_provider import ServiceProvider +from .keycloak.keycloak_admin import KeycloakAdmin +from .keycloak.keycloak_client import KeycloakClient +from .schema._administration.api_key_dao import ApiKeyDao +from .schema._administration.auth_user_dao import AuthUserDao +from .schema._permission.api_key_permission_dao import ApiKeyPermissionDao +from .schema._permission.permission_dao import PermissionDao +from .schema._permission.role_dao import RoleDao +from .schema._permission.role_permission_dao import RolePermissionDao +from .schema._permission.role_user_dao import RoleUserDao class AuthModule(Module): dependencies = [DatabaseModule, (MySQLModule, PostgresModule)] + config = [KeycloakSettings] + singleton = [ + KeycloakClient, + KeycloakAdmin, + AuthUserDao, + ApiKeyDao, + ApiKeyPermissionDao, + PermissionDao, + RoleDao, + RolePermissionDao, + RoleUserDao, + ] + scoped = [] + transient = [] @staticmethod - def register(collection: ServiceCollection): - from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin - from cpl.auth.keycloak.keycloak_client import KeycloakClient - from .schema._administration.api_key_dao import ApiKeyDao - from .schema._administration.auth_user_dao import AuthUserDao - from .schema._permission.api_key_permission_dao import ApiKeyPermissionDao - from .schema._permission.permission_dao import PermissionDao - from .schema._permission.role_dao import RoleDao - from .schema._permission.role_permission_dao import RolePermissionDao - from .schema._permission.role_user_dao import RoleUserDao + def configure(provider: ServiceProvider): + paths = { + ServerTypes.POSTGRES: "scripts/postgres", + ServerTypes.MYSQL: "scripts/mysql", + } - collection.add_singleton(KeycloakClient) - collection.add_singleton(KeycloakAdmin) + DatabaseModule.with_migrations( + provider, str(os.path.join(os.path.dirname(os.path.realpath(__file__)), paths[ServerType.server_type])) + ) - collection.add_singleton(AuthUserDao) - collection.add_singleton(ApiKeyDao) - collection.add_singleton(ApiKeyPermissionDao) - collection.add_singleton(PermissionDao) - collection.add_singleton(RoleDao) - collection.add_singleton(RolePermissionDao) - collection.add_singleton(RoleUserDao) + @staticmethod + def with_permissions(*permissions: Type[Enum]): + from cpl.auth.permission.permissions_registry import PermissionsRegistry - provider = collection.build() - migration_service: MigrationService = provider.get_service(MigrationService) - if ServerType.server_type == ServerTypes.POSTGRES: - migration_service.with_directory( - os.path.join(os.path.dirname(os.path.realpath(__file__)), "scripts/postgres") - ) - elif ServerType.server_type == ServerTypes.MYSQL: - migration_service.with_directory(os.path.join(os.path.dirname(os.path.realpath(__file__)), "scripts/mysql")) + for perm in permissions: + PermissionsRegistry.with_enum(perm) diff --git a/src/cpl-auth/cpl/auth/permission/permission_module.py b/src/cpl-auth/cpl/auth/permission/permission_module.py index a6866120..16955c57 100644 --- a/src/cpl-auth/cpl/auth/permission/permission_module.py +++ b/src/cpl-auth/cpl/auth/permission/permission_module.py @@ -4,14 +4,14 @@ from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.database.abc.data_seeder_abc import DataSeederABC from cpl.database.database_module import DatabaseModule -from cpl.dependency.module import Module +from cpl.dependency.module.module import Module from cpl.dependency.service_collection import ServiceCollection class PermissionsModule(Module): dependencies = [DatabaseModule, AuthModule] + singleton = [(DataSeederABC, PermissionSeeder)] @staticmethod def register(collection: ServiceCollection): - collection.add_singleton(DataSeederABC, PermissionSeeder) PermissionsRegistry.with_enum(Permissions) diff --git a/src/cpl-core/cpl/core/errors.py b/src/cpl-core/cpl/core/errors.py index 2fb0e199..15beaaa8 100644 --- a/src/cpl-core/cpl/core/errors.py +++ b/src/cpl-core/cpl/core/errors.py @@ -7,10 +7,10 @@ def dependency_error(src: str, package_name: str, e: ImportError = None) -> None Console.error(f"'{package_name}' is required to use feature: {src}. Please install it and try again.") tb = traceback.format_exc() if not tb.startswith("NoneType: None"): - Console.write_line("->", tb) + Console.error("->", tb) elif e is not None: - Console.write_line("->", str(e)) + Console.error(f"-> {str(e)}") exit(1) @@ -19,9 +19,9 @@ def module_dependency_error(src: str, module: str, e: ImportError = None) -> Non Console.error(f"'{module}' is required by '{src}'. Please initialize it with `add_module({module})`.") tb = traceback.format_exc() if not tb.startswith("NoneType: None"): - Console.write_line("->", tb) + Console.error("->", tb) elif e is not None: - Console.write_line("->", str(e)) + Console.error(f"-> {str(e)}") exit(1) diff --git a/src/cpl-database/cpl/database/__init__.py b/src/cpl-database/cpl/database/__init__.py index a2d4dc88..b9576582 100644 --- a/src/cpl-database/cpl/database/__init__.py +++ b/src/cpl-database/cpl/database/__init__.py @@ -1,36 +1,5 @@ -import os - -from cpl.application.abc import ApplicationABC as _ApplicationABC from . import mysql as _mysql from . import postgres as _postgres from .database_module import DatabaseModule -from .table_manager import TableManager from .logger import DBLogger - - -def _with_migrations(self: _ApplicationABC, *paths: str | list[str]) -> _ApplicationABC: - from cpl.database.service.migration_service import MigrationService - - migration_service = self._services.get_service(MigrationService) - migration_service.with_directory(os.path.join(os.path.dirname(os.path.abspath(__file__)), "scripts")) - - if isinstance(paths, str): - paths = [paths] - - for path in paths: - migration_service.with_directory(path) - - return self - - -def _with_seeders(self: _ApplicationABC) -> _ApplicationABC: - from cpl.database.service.seeder_service import SeederService - from cpl.application.host import Host - - seeder_service: SeederService = self._services.get_service(SeederService) - Host.run(seeder_service.seed) - return self - - -_ApplicationABC.extend(_ApplicationABC.with_migrations, _with_migrations) -_ApplicationABC.extend(_ApplicationABC.with_seeders, _with_seeders) +from .table_manager import TableManager diff --git a/src/cpl-database/cpl/database/database_module.py b/src/cpl-database/cpl/database/database_module.py index 01b23497..38feb09d 100644 --- a/src/cpl-database/cpl/database/database_module.py +++ b/src/cpl-database/cpl/database/database_module.py @@ -1,18 +1,33 @@ +from cpl.database.model.database_settings import DatabaseSettings from cpl.database.mysql.mysql_module import MySQLModule from cpl.database.postgres.postgres_module import PostgresModule -from cpl.dependency.module import Module -from cpl.dependency.service_collection import ServiceCollection +from cpl.database.schema.executed_migration_dao import ExecutedMigrationDao +from cpl.database.service.migration_service import MigrationService +from cpl.database.service.seeder_service import SeederService +from cpl.dependency.module.module import Module +from cpl.dependency.service_provider import ServiceProvider class DatabaseModule(Module): dependencies = [(MySQLModule, PostgresModule)] + config = [DatabaseSettings] + singleton = [ + ExecutedMigrationDao, + MigrationService, + SeederService, + ] + + @classmethod + def configure(cls, provider: ServiceProvider): ... @staticmethod - def register(collection: ServiceCollection): - from cpl.database.schema import ExecutedMigrationDao + def with_migrations(services: ServiceProvider, *paths: str | list[str]): from cpl.database.service.migration_service import MigrationService - from cpl.database.service.seeder_service import SeederService - collection.add_singleton(ExecutedMigrationDao) - collection.add_singleton(MigrationService) - collection.add_singleton(SeederService) + migration_service = services.get_service(MigrationService) + + if isinstance(paths, str): + paths = [paths] + + for path in paths: + migration_service.with_directory(path) diff --git a/src/cpl-database/cpl/database/mysql/mysql_module.py b/src/cpl-database/cpl/database/mysql/mysql_module.py index 3aff86c4..9c35eee0 100644 --- a/src/cpl-database/cpl/database/mysql/mysql_module.py +++ b/src/cpl-database/cpl/database/mysql/mysql_module.py @@ -1,18 +1,17 @@ from cpl.core.configuration.configuration import Configuration +from cpl.database.abc.db_context_abc import DBContextABC +from cpl.database.model.database_settings import DatabaseSettings from cpl.database.model.server_type import ServerTypes, ServerType -from cpl.dependency.module import Module +from cpl.database.mysql.db_context import DBContext +from cpl.dependency.module.module import Module from cpl.dependency.service_collection import ServiceCollection class MySQLModule(Module): - dependencies = [] + config = [DatabaseSettings] + singleton = [(DBContextABC, DBContext)] @staticmethod def register(collection: ServiceCollection): - from cpl.database.abc.db_context_abc import DBContextABC - from cpl.database.mysql.db_context import DBContext - ServerType.set_server_type(ServerTypes(ServerTypes.MYSQL.value)) Configuration.set("DB_DEFAULT_PORT", 3306) - - collection.add_singleton(DBContextABC, DBContext) diff --git a/src/cpl-database/cpl/database/postgres/postgres_module.py b/src/cpl-database/cpl/database/postgres/postgres_module.py index 679a2b00..c476fac9 100644 --- a/src/cpl-database/cpl/database/postgres/postgres_module.py +++ b/src/cpl-database/cpl/database/postgres/postgres_module.py @@ -1,18 +1,17 @@ from cpl.core.configuration.configuration import Configuration +from cpl.database.abc.db_context_abc import DBContextABC +from cpl.database.model.database_settings import DatabaseSettings from cpl.database.model.server_type import ServerTypes, ServerType -from cpl.dependency.module import Module +from cpl.database.postgres.db_context import DBContext +from cpl.dependency.module.module import Module from cpl.dependency.service_collection import ServiceCollection class PostgresModule(Module): - dependencies = [] + config = [DatabaseSettings] + singleton = [(DBContextABC, DBContext)] @staticmethod def register(collection: ServiceCollection): - from cpl.database.abc.db_context_abc import DBContextABC - from cpl.database.postgres.db_context import DBContext - ServerType.set_server_type(ServerTypes(ServerTypes.POSTGRES.value)) Configuration.set("DB_DEFAULT_PORT", 5432) - - collection.add_singleton(DBContextABC, DBContext) diff --git a/src/cpl-database/cpl/database/service/__init__.py b/src/cpl-database/cpl/database/service/__init__.py index e69de29b..9102df38 100644 --- a/src/cpl-database/cpl/database/service/__init__.py +++ b/src/cpl-database/cpl/database/service/__init__.py @@ -0,0 +1,2 @@ +from .seeder_service import SeederService +from .migration_service import MigrationService diff --git a/src/cpl-database/cpl/database/service/migration_service.py b/src/cpl-database/cpl/database/service/migration_service.py index 7c698ace..631c903b 100644 --- a/src/cpl-database/cpl/database/service/migration_service.py +++ b/src/cpl-database/cpl/database/service/migration_service.py @@ -7,13 +7,12 @@ from cpl.database.model.migration import Migration from cpl.database.model.server_type import ServerType, ServerTypes from cpl.database.schema.executed_migration import ExecutedMigration from cpl.database.schema.executed_migration_dao import ExecutedMigrationDao -from cpl.dependency.hosted.startup_task import StartupTask +from cpl.dependency.hosted import StartupTask class MigrationService(StartupTask): def __init__(self, logger: DBLogger, db: DBContextABC, executed_migration_dao: ExecutedMigrationDao): - StartupTask.__init__(self) self._logger = logger self._db = db self._executed_migration_dao = executed_migration_dao @@ -24,12 +23,23 @@ class MigrationService(StartupTask): self.with_directory(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../scripts/postgres")) elif ServerType.server_type == ServerTypes.MYSQL: self.with_directory(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../scripts/mysql")) + else: + raise Exception("Unsupported database type") async def run(self): await self._execute(self._load_scripts()) def with_directory(self, directory: str) -> "MigrationService": - self._script_directories.append(directory) + cpl_rel_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../../..") + cpl_abs_path = os.path.abspath(cpl_rel_path) + + if directory.startswith(cpl_abs_path) or os.path.abspath(directory).startswith(cpl_abs_path): + if len(self._script_directories) > 0: + self._script_directories.insert(1, directory) + else: + self._script_directories.append(directory) + else: + self._script_directories.append(directory) return self async def _get_migration_history(self) -> list[ExecutedMigration]: diff --git a/src/cpl-database/cpl/database/service/seeder_service.py b/src/cpl-database/cpl/database/service/seeder_service.py index 74491f8c..ddebd76d 100644 --- a/src/cpl-database/cpl/database/service/seeder_service.py +++ b/src/cpl-database/cpl/database/service/seeder_service.py @@ -1,15 +1,17 @@ from cpl.database.abc.data_seeder_abc import DataSeederABC from cpl.database.logger import DBLogger from cpl.dependency import ServiceProvider +from cpl.dependency.hosted import StartupTask -class SeederService: +class SeederService(StartupTask): def __init__(self, provider: ServiceProvider): + StartupTask.__init__(self) self._provider = provider self._logger = provider.get_service(DBLogger) - async def seed(self): + async def run(self): seeders = self._provider.get_services(DataSeederABC) self._logger.debug(f"Found {len(seeders)} seeders") for seeder in seeders: diff --git a/src/cpl-dependency/cpl/dependency/module.py b/src/cpl-dependency/cpl/dependency/module.py deleted file mode 100644 index e6376638..00000000 --- a/src/cpl-dependency/cpl/dependency/module.py +++ /dev/null @@ -1,23 +0,0 @@ -from abc import abstractmethod, ABC -from inspect import isclass - - -class Module(ABC): - __REQUIRED_VARS = ["dependencies"] - - def __init_subclass__(cls): - super().__init_subclass__() - 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 - @abstractmethod - def register(collection: "ServiceCollection"): ... diff --git a/src/cpl-dependency/cpl/dependency/module/__init__.py b/src/cpl-dependency/cpl/dependency/module/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/cpl-dependency/cpl/dependency/module/module.py b/src/cpl-dependency/cpl/dependency/module/module.py new file mode 100644 index 00000000..89d6ed0f --- /dev/null +++ b/src/cpl-dependency/cpl/dependency/module/module.py @@ -0,0 +1,10 @@ +from cpl.dependency.module.module_abc import ModuleABC + + +class Module(ModuleABC): + + @staticmethod + def register(collection: "ServiceCollection"): ... + + @staticmethod + def configure(provider: "ServiceProvider"): ... diff --git a/src/cpl-dependency/cpl/dependency/module/module_abc.py b/src/cpl-dependency/cpl/dependency/module/module_abc.py new file mode 100644 index 00000000..971a721c --- /dev/null +++ b/src/cpl-dependency/cpl/dependency/module/module_abc.py @@ -0,0 +1,60 @@ +from abc import ABC, abstractmethod +from inspect import isclass + +from cpl.core.configuration import ConfigurationModelABC + + +class ModuleABC(ABC): + __OPTIONAL_VARS = ["dependencies", "configuration", "singleton", "scoped", "transient", "hosted"] + + def __init_subclass__(cls): + super().__init_subclass__() + + if f"{cls.__module__}.{cls.__name__}" == "cpl.dependency.module.module.Module": + return + + for var in cls.__OPTIONAL_VARS: + if not hasattr(cls, var): + continue + + value = getattr(cls, var) + + if not isinstance(value, list): + raise TypeError(f"'{var}' attribute of {cls.__name__} must be a list, not {type(value).__name__}") + + for dep in value: + if var == "config": + if not isclass(dep) or not issubclass(dep, ConfigurationModelABC): + raise TypeError( + f"Invalid config {dep} in {cls.__name__}: must be subclass of ConfigurationModelABC" + ) + elif var == "dependencies": + if not isinstance(dep, (list, tuple)) and not isclass(dep): + raise TypeError(f"Invalid dependency {dep} in {cls.__name__}") + else: + if not isinstance(dep, tuple) and not isclass(dep): + raise TypeError(f"Invalid {var} {dep} in {cls.__name__}") + + @classmethod + def get_singleton(cls): + return getattr(cls, "singleton", []) + + @classmethod + def get_scoped(cls): + return getattr(cls, "scoped", []) + + @classmethod + def get_transient(cls): + return getattr(cls, "transient", []) + + @classmethod + def get_hosted(cls): + return getattr(cls, "hosted", []) + + @staticmethod + @abstractmethod + def register(collection: "ServiceCollection"): ... + + @staticmethod + @abstractmethod + def configure(provider: "ServiceProvider"): ... diff --git a/src/cpl-dependency/cpl/dependency/module/module_protocol.py b/src/cpl-dependency/cpl/dependency/module/module_protocol.py new file mode 100644 index 00000000..a5375d2e --- /dev/null +++ b/src/cpl-dependency/cpl/dependency/module/module_protocol.py @@ -0,0 +1,17 @@ +from typing import Protocol + +from cpl.dependency.typing import TService, TModule, TConfig + + +class ModuleProtocol(Protocol): + dependencies: list[TModule | TService] = [] + config: list[TConfig] = [] + singleton: list[TService] = [] + scoped: list[TService] = [] + transient: list[TService] = [] + + @staticmethod + def register(collection: "ServiceCollection"): ... + + @staticmethod + def configure(provider: "ServiceProvider"): ... diff --git a/src/cpl-dependency/cpl/dependency/service_collection.py b/src/cpl-dependency/cpl/dependency/service_collection.py index 8086ed5a..8747ed59 100644 --- a/src/cpl-dependency/cpl/dependency/service_collection.py +++ b/src/cpl-dependency/cpl/dependency/service_collection.py @@ -6,7 +6,7 @@ from cpl.core.log.logger_abc import LoggerABC from cpl.core.typing import T, Service from cpl.core.utils.cache import Cache from cpl.dependency.hosted.startup_task import StartupTask -from cpl.dependency.module import Module +from cpl.dependency.module.module import Module from cpl.dependency.service_descriptor import ServiceDescriptor from cpl.dependency.service_lifetime import ServiceLifetimeEnum from cpl.dependency.service_provider import ServiceProvider @@ -20,10 +20,10 @@ class ServiceCollection: def __init__(self): self._service_descriptors: list[ServiceDescriptor] = [] - self._loaded_modules: set[str] = set() + self._loaded_modules: set[TModule] = set() @property - def loaded_modules(self) -> set[str]: + def loaded_modules(self) -> set[TModule]: return self._loaded_modules def _check_dependency(self, module: TModule, dependency: TModule | TService, optional: bool = False) -> bool: @@ -42,7 +42,7 @@ class ServiceCollection: module_dependency_error(module.__name__, dependency.__name__) - if dependency.__name__ not in self._loaded_modules: + if dependency not in self._loaded_modules: if optional: return False @@ -50,6 +50,63 @@ class ServiceCollection: return True + def _add_module_service(self, service: TService | tuple[TService, TService], lifetime: ServiceLifetimeEnum): + args = () + + if isinstance(service, tuple): + if len(service) != 2: + raise ValueError("Service must be a tuple in the format (XABC, X)") + + k, v = service + if not (isinstance(k, type) and isinstance(v, type)): + raise ValueError("Service tuple must have elements in the format (XABC, X)") + args = (k, v) + else: + if not isinstance(service, type): + raise ValueError("Service must be a type or a tuple of two types") + args = (service,) + + match lifetime: + case ServiceLifetimeEnum.singleton: + self.add_singleton(*args) + case ServiceLifetimeEnum.scoped: + self.add_scoped(*args) + case ServiceLifetimeEnum.transient: + self.add_transient(*args) + case ServiceLifetimeEnum.hosted: + self.add_hosted_service(*args) + case _: + raise ValueError(f"Unknown service lifetime: {lifetime}") + + def _add_module_services(self, module: TModule): + for s in module.get_singleton(): + self._add_module_service(s, ServiceLifetimeEnum.singleton) + + for s in module.get_scoped(): + self._add_module_service(s, ServiceLifetimeEnum.scoped) + + for s in module.get_transient(): + self._add_module_service(s, ServiceLifetimeEnum.transient) + + for s in module.get_hosted(): + self._add_module_service(s, ServiceLifetimeEnum.hosted) + + def _add_module_configuration(self, module: TModule): + from cpl.core.configuration.configuration import Configuration + from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC + + configs = getattr(module, "configuration", []) + for config in configs: + if not issubclass(config, ConfigurationModelABC): + raise TypeError( + f"Invalid config {config} in {module.__name__}: must be subclass of ConfigurationModelABC" + ) + + cfg = Configuration.get(config) + if cfg is None: + continue + self.add_singleton(cfg) + def _check_dependencies(self, module: TModule): dependencies: list[TModule | Type] = getattr(module, "dependencies", []) for dependency in dependencies: @@ -83,7 +140,7 @@ class ServiceCollection: self._service_descriptors.append(ServiceDescriptor(service, lifetime, base_type)) def _add_descriptor_by_lifetime( - self, service_type: TService | T, lifetime: ServiceLifetimeEnum, service: Callable = None + self, service_type: TService | T, lifetime: ServiceLifetimeEnum, service: Callable = None ): if service is not None: self._add_descriptor(service, lifetime, service_type) @@ -120,16 +177,16 @@ class ServiceCollection: assert isclass(module), "Module must be a Module" assert issubclass(module, Module), f"Module must be subclass of {Module.__name__}" - name = module.__name__ if module in self._modules: - raise ValueError(f"Module {name} is already registered") + raise ValueError(f"Module {module.__name__} is already registered") self._check_dependencies(module) + self._add_module_configuration(module) + self._add_module_services(module) + module.register(self) - module().register(self) - - if name not in self._loaded_modules: - self._loaded_modules.add(name) + if module not in self._loaded_modules: + self._loaded_modules.add(module) return self diff --git a/src/cpl-dependency/cpl/dependency/service_provider.py b/src/cpl-dependency/cpl/dependency/service_provider.py index aa6fcbb3..dc3250ab 100644 --- a/src/cpl-dependency/cpl/dependency/service_provider.py +++ b/src/cpl-dependency/cpl/dependency/service_provider.py @@ -24,7 +24,9 @@ class ServiceProvider: for descriptor in self._service_descriptors: if typing.get_origin(service_type) is None and ( - descriptor.service_type == service_type or issubclass(descriptor.base_type, service_type) + descriptor.service_type == service_type + or typing.get_origin(descriptor.base_type) is None + and issubclass(descriptor.base_type, service_type) ): return descriptor diff --git a/src/cpl-dependency/cpl/dependency/typing.py b/src/cpl-dependency/cpl/dependency/typing.py index 78b64652..032b02b5 100644 --- a/src/cpl-dependency/cpl/dependency/typing.py +++ b/src/cpl-dependency/cpl/dependency/typing.py @@ -1,9 +1,12 @@ from typing import Type +from cpl.core.configuration import ConfigurationModelABC from cpl.core.typing import T from cpl.dependency.hosted import StartupTask -from cpl.dependency.module import Module +from cpl.dependency.module.module import Module TModule = Type[Module] +Modules = set[TModule] TService = Type[T] +TConfig = Type[ConfigurationModelABC] TStartupTask = Type[StartupTask] diff --git a/src/cpl-mail/cpl/mail/mail_module.py b/src/cpl-mail/cpl/mail/mail_module.py index 3594bbe0..157129fd 100644 --- a/src/cpl-mail/cpl/mail/mail_module.py +++ b/src/cpl-mail/cpl/mail/mail_module.py @@ -1,13 +1,8 @@ -from cpl.dependency.service_collection import ServiceCollection -from cpl.dependency.module import Module, TModule +from cpl.dependency.module.module import Module + +from cpl.mail.abc.email_client_abc import EMailClientABC +from cpl.mail.email_client import EMailClient class MailModule(Module): - dependencies = [] - - @staticmethod - def register(collection: ServiceCollection): - from cpl.mail.abc.email_client_abc import EMailClientABC - from cpl.mail.email_client import EMailClient - - collection.add_singleton(EMailClientABC, EMailClient) + singleton = [(EMailClientABC, EMailClient)] diff --git a/src/cpl-translation/cpl/translation/translation_module.py b/src/cpl-translation/cpl/translation/translation_module.py index 19444fd4..c3807c1d 100644 --- a/src/cpl-translation/cpl/translation/translation_module.py +++ b/src/cpl-translation/cpl/translation/translation_module.py @@ -1,13 +1,7 @@ -from cpl.dependency.service_collection import ServiceCollection -from cpl.dependency.module import Module +from cpl.dependency.module.module import Module +from cpl.translation.translation_service import TranslationService from cpl.translation.translation_service_abc import TranslationServiceABC class TranslationModule(Module): - dependencies = [] - - @staticmethod - def register(collection: ServiceCollection): - from cpl.translation.translation_service import TranslationService - - collection.add_singleton(TranslationServiceABC, TranslationService) + singleton = [(TranslationServiceABC, TranslationService)]