Better modules
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
Build on push / prepare (push) Successful in 9s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 19s
Build on push / dependency (push) Successful in 17s
Build on push / mail (push) Successful in 16s
Build on push / translation (push) Successful in 16s
Build on push / application (push) Successful in 18s
Build on push / database (push) Successful in 19s
Build on push / auth (push) Successful in 17s
Build on push / api (push) Successful in 14s

This commit is contained in:
2025-09-26 12:55:00 +02:00
parent 56a16cbeba
commit c410a692be
29 changed files with 341 additions and 223 deletions

View File

@@ -35,7 +35,6 @@ def main():
app = builder.build() app = builder.build()
app.with_logging() app.with_logging()
app.with_database()
app.with_authentication() app.with_authentication()
app.with_authorization() app.with_authorization()

View File

@@ -4,6 +4,7 @@ from cpl.core.console import Console
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.log import LoggerABC from cpl.core.log import LoggerABC
from cpl.dependency import ServiceProvider from cpl.dependency import ServiceProvider
from cpl.dependency.typing import Modules
from model.city import City from model.city import City
from model.city_dao import CityDao from model.city_dao import CityDao
from model.user import User from model.user import User
@@ -11,8 +12,8 @@ from model.user_dao import UserDao
class Application(ApplicationABC): class Application(ApplicationABC):
def __init__(self, services: ServiceProvider): def __init__(self, services: ServiceProvider, modules: Modules):
ApplicationABC.__init__(self, services) ApplicationABC.__init__(self, services, modules)
self._logger = services.get_service(LoggerABC) self._logger = services.get_service(LoggerABC)

View File

@@ -3,6 +3,7 @@ from cpl.application import ApplicationBuilder
from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.auth.permission.permissions_registry import PermissionsRegistry
from cpl.core.console import Console from cpl.core.console import Console
from cpl.core.log import LogLevel from cpl.core.log import LogLevel
from cpl.database import DatabaseModule
from custom_permissions import CustomPermissions from custom_permissions import CustomPermissions
from startup import Startup from startup import Startup
@@ -10,13 +11,12 @@ from startup import Startup
def main(): def main():
builder = ApplicationBuilder(Application).with_startup(Startup) builder = ApplicationBuilder(Application).with_startup(Startup)
builder.services.add_logging() builder.services.add_logging()
app = builder.build() app = builder.build()
app.with_logging(LogLevel.trace) app.with_logging(LogLevel.trace)
app.with_permissions(CustomPermissions) app.with_permissions(CustomPermissions)
app.with_migrations("./scripts") app.with_migrations("./scripts")
app.with_seeders() # app.with_seeders()
Console.write_line(CustomPermissions.test.value in PermissionsRegistry.get()) Console.write_line(CustomPermissions.test.value in PermissionsRegistry.get())
app.run() app.run()

View File

@@ -6,7 +6,7 @@ from cpl.auth.permission.permission_module import PermissionsModule
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.log import Logger, LoggerABC 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.abc.data_access_object_abc import DataAccessObjectABC
from cpl.database.mysql.mysql_module import MySQLModule from cpl.database.mysql.mysql_module import MySQLModule
from cpl.dependency import ServiceCollection from cpl.dependency import ServiceCollection
@@ -25,6 +25,7 @@ class Startup(StartupABC):
@staticmethod @staticmethod
async def configure_services(services: ServiceCollection): async def configure_services(services: ServiceCollection):
services.add_module(MySQLModule) services.add_module(MySQLModule)
services.add_module(DatabaseModule)
services.add_module(AuthModule) services.add_module(AuthModule)
services.add_module(PermissionsModule) services.add_module(PermissionsModule)

View File

@@ -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.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 from cpl.dependency import ServiceCollection
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.module.module import Module
class ApiModule(Module): class ApiModule(Module):
dependencies = [] config = [ApiSettings]
singleton = [
PolicyRegistry,
RouteRegistry,
]
@staticmethod @staticmethod
def register(collection: ServiceCollection): 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(DatabaseModule)
collection.add_module(AuthModule) collection.add_module(AuthModule)
collection.add_module(PermissionsModule) collection.add_module(PermissionsModule)
collection.add_singleton(PolicyRegistry)
collection.add_singleton(RouteRegistry)

View File

@@ -10,7 +10,7 @@ from starlette.requests import Request
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from starlette.types import ExceptionHandler 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.error import APIError
from cpl.api.logger import APILogger from cpl.api.logger import APILogger
from cpl.api.middleware.authentication import AuthenticationMiddleware 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.router import Router
from cpl.api.settings import ApiSettings from cpl.api.settings import ApiSettings
from cpl.api.typing import HTTPMethods, PartialMiddleware, PolicyResolver 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.application.abc.application_abc import ApplicationABC
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.core.configuration import Configuration from cpl.core.configuration.configuration import Configuration
from cpl.dependency.inject import inject from cpl.dependency.inject import inject
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
from cpl.dependency.typing import Modules
PolicyInput = Union[dict[str, PolicyResolver], Policy] PolicyInput = Union[dict[str, PolicyResolver], Policy]
class WebApp(ApplicationABC): class WebApp(ApplicationABC):
def __init__(self, services: ServiceProvider): def __init__(self, services: ServiceProvider, modules: Modules):
super().__init__(services, [AuthModule, PermissionsModule, ApiModule]) super().__init__(services, modules, [AuthModule, PermissionsModule, ApiModule])
self._app: Starlette | None = None self._app: Starlette | None = None
self._logger = services.get_service(APILogger) self._logger = services.get_service(APILogger)
@@ -78,11 +78,6 @@ class WebApp(ApplicationABC):
self._logger.debug(f"Allowed origins: {origins}") self._logger.debug(f"Allowed origins: {origins}")
return origins.split(",") return origins.split(",")
def with_database(self) -> Self:
self.with_migrations()
self.with_seeders()
return self
def with_app(self, app: Starlette) -> Self: def with_app(self, app: Starlette) -> Self:
assert app is not None, "app must not be None" assert app is not None, "app must not be None"
assert isinstance(app, Starlette), "app must be an instance of Starlette" assert isinstance(app, Starlette), "app must be an instance of Starlette"

View File

@@ -2,10 +2,12 @@ from abc import ABC, abstractmethod
from typing import Callable, Self from typing import Callable, Self
from cpl.application.host import Host 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_level import LogLevel
from cpl.core.log.log_settings import LogSettings from cpl.core.log.log_settings import LogSettings
from cpl.core.log.logger_abc import LoggerABC from cpl.core.log.logger_abc import LoggerABC
from cpl.dependency.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
from cpl.dependency.typing import TModule
def __not_implemented__(package: str, func: Callable): def __not_implemented__(package: str, func: Callable):
@@ -20,17 +22,6 @@ class ApplicationABC(ABC):
Contains instances of prepared objects 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 @classmethod
def extend(cls, name: str | Callable, func: Callable[[Self], Self]): def extend(cls, name: str | Callable, func: Callable[[Self], Self]):
r"""Extend the Application with a custom method r"""Extend the Application with a custom method
@@ -47,6 +38,30 @@ class ApplicationABC(ABC):
setattr(cls, name, func) setattr(cls, name, func)
return cls 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): def with_logging(self, level: LogLevel = None):
if level is None: if level is None:
from cpl.core.configuration.configuration import Configuration from cpl.core.configuration.configuration import Configuration
@@ -57,14 +72,21 @@ class ApplicationABC(ABC):
logger = self._services.get_service(LoggerABC) logger = self._services.get_service(LoggerABC)
logger.set_level(level) logger.set_level(level)
def with_permissions(self, *args, **kwargs): def with_permissions(self, *args):
__not_implemented__("cpl-auth", self.with_permissions) try:
from cpl.auth import AuthModule
def with_migrations(self, *args, **kwargs): AuthModule.with_permissions(*args)
__not_implemented__("cpl-database", self.with_migrations) except ImportError:
__not_implemented__("cpl-auth", self.with_permissions)
def with_seeders(self, *args, **kwargs): def with_migrations(self, *args):
__not_implemented__("cpl-database", self.with_seeders) 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): def with_extension(self, func: Callable[[Self, ...], None], *args, **kwargs):
r"""Extend the Application with a custom method r"""Extend the Application with a custom method
@@ -84,6 +106,11 @@ class ApplicationABC(ABC):
Called by custom Application.main Called by custom Application.main
""" """
try: 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) Host.run_app(self.main)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass

View File

@@ -43,19 +43,6 @@ class ApplicationBuilder(Generic[TApp]):
return provider 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": def with_startup(self, startup: Type[StartupABC]) -> "ApplicationBuilder":
self._startup = startup self._startup = startup
return self return self
@@ -84,6 +71,6 @@ class ApplicationBuilder(Generic[TApp]):
Host.run(extension.run, self.service_provider) Host.run(extension.run, self.service_provider)
use_root_provider(self._services.build()) use_root_provider(self._services.build())
app = self._app(self.service_provider) app = self._app(self.service_provider, self._services.loaded_modules)
self.validate_app_required_modules(app) app.validate_app_required_modules()
return app return app

View File

@@ -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 import permission as _permission
from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin as _KeycloakAdmin from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin as _KeycloakAdmin
from cpl.auth.keycloak.keycloak_client import KeycloakClient as _KeycloakClient from cpl.auth.keycloak.keycloak_client import KeycloakClient as _KeycloakClient
from .auth_module import AuthModule
from .keycloak_settings import KeycloakSettings from .keycloak_settings import KeycloakSettings
from .logger import AuthLogger 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)

View File

@@ -1,45 +1,56 @@
import os 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.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.mysql.mysql_module import MySQLModule
from cpl.database.postgres.postgres_module import PostgresModule from cpl.database.postgres.postgres_module import PostgresModule
from cpl.database.service.migration_service import MigrationService from cpl.dependency.module.module import Module
from cpl.dependency.module import Module from cpl.dependency.service_provider import ServiceProvider
from cpl.dependency.service_collection import ServiceCollection 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): class AuthModule(Module):
dependencies = [DatabaseModule, (MySQLModule, PostgresModule)] dependencies = [DatabaseModule, (MySQLModule, PostgresModule)]
config = [KeycloakSettings]
singleton = [
KeycloakClient,
KeycloakAdmin,
AuthUserDao,
ApiKeyDao,
ApiKeyPermissionDao,
PermissionDao,
RoleDao,
RolePermissionDao,
RoleUserDao,
]
scoped = []
transient = []
@staticmethod @staticmethod
def register(collection: ServiceCollection): def configure(provider: ServiceProvider):
from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin paths = {
from cpl.auth.keycloak.keycloak_client import KeycloakClient ServerTypes.POSTGRES: "scripts/postgres",
from .schema._administration.api_key_dao import ApiKeyDao ServerTypes.MYSQL: "scripts/mysql",
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
collection.add_singleton(KeycloakClient) DatabaseModule.with_migrations(
collection.add_singleton(KeycloakAdmin) provider, str(os.path.join(os.path.dirname(os.path.realpath(__file__)), paths[ServerType.server_type]))
)
collection.add_singleton(AuthUserDao) @staticmethod
collection.add_singleton(ApiKeyDao) def with_permissions(*permissions: Type[Enum]):
collection.add_singleton(ApiKeyPermissionDao) from cpl.auth.permission.permissions_registry import PermissionsRegistry
collection.add_singleton(PermissionDao)
collection.add_singleton(RoleDao)
collection.add_singleton(RolePermissionDao)
collection.add_singleton(RoleUserDao)
provider = collection.build() for perm in permissions:
migration_service: MigrationService = provider.get_service(MigrationService) PermissionsRegistry.with_enum(perm)
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"))

View File

@@ -4,14 +4,14 @@ 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.database.database_module import DatabaseModule 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 from cpl.dependency.service_collection import ServiceCollection
class PermissionsModule(Module): class PermissionsModule(Module):
dependencies = [DatabaseModule, AuthModule] dependencies = [DatabaseModule, AuthModule]
singleton = [(DataSeederABC, PermissionSeeder)]
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):
collection.add_singleton(DataSeederABC, PermissionSeeder)
PermissionsRegistry.with_enum(Permissions) PermissionsRegistry.with_enum(Permissions)

View File

@@ -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.") Console.error(f"'{package_name}' is required to use feature: {src}. Please install it and try again.")
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.error("->", tb)
elif e is not None: elif e is not None:
Console.write_line("->", str(e)) Console.error(f"-> {str(e)}")
exit(1) 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})`.") 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.error("->", tb)
elif e is not None: elif e is not None:
Console.write_line("->", str(e)) Console.error(f"-> {str(e)}")
exit(1) exit(1)

View File

@@ -1,36 +1,5 @@
import os
from cpl.application.abc import ApplicationABC as _ApplicationABC
from . import mysql as _mysql from . import mysql as _mysql
from . import postgres as _postgres from . import postgres as _postgres
from .database_module import DatabaseModule from .database_module import DatabaseModule
from .table_manager import TableManager
from .logger import DBLogger from .logger import DBLogger
from .table_manager import TableManager
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)

View File

@@ -1,18 +1,33 @@
from cpl.database.model.database_settings import DatabaseSettings
from cpl.database.mysql.mysql_module import MySQLModule from cpl.database.mysql.mysql_module import MySQLModule
from cpl.database.postgres.postgres_module import PostgresModule from cpl.database.postgres.postgres_module import PostgresModule
from cpl.dependency.module import Module from cpl.database.schema.executed_migration_dao import ExecutedMigrationDao
from cpl.dependency.service_collection import ServiceCollection 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): class DatabaseModule(Module):
dependencies = [(MySQLModule, PostgresModule)] dependencies = [(MySQLModule, PostgresModule)]
config = [DatabaseSettings]
singleton = [
ExecutedMigrationDao,
MigrationService,
SeederService,
]
@classmethod
def configure(cls, provider: ServiceProvider): ...
@staticmethod @staticmethod
def register(collection: ServiceCollection): def with_migrations(services: ServiceProvider, *paths: str | list[str]):
from cpl.database.schema import ExecutedMigrationDao
from cpl.database.service.migration_service import MigrationService from cpl.database.service.migration_service import MigrationService
from cpl.database.service.seeder_service import SeederService
collection.add_singleton(ExecutedMigrationDao) migration_service = services.get_service(MigrationService)
collection.add_singleton(MigrationService)
collection.add_singleton(SeederService) if isinstance(paths, str):
paths = [paths]
for path in paths:
migration_service.with_directory(path)

View File

@@ -1,18 +1,17 @@
from cpl.core.configuration.configuration import Configuration 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.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 from cpl.dependency.service_collection import ServiceCollection
class MySQLModule(Module): class MySQLModule(Module):
dependencies = [] config = [DatabaseSettings]
singleton = [(DBContextABC, DBContext)]
@staticmethod @staticmethod
def register(collection: ServiceCollection): 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)) ServerType.set_server_type(ServerTypes(ServerTypes.MYSQL.value))
Configuration.set("DB_DEFAULT_PORT", 3306) Configuration.set("DB_DEFAULT_PORT", 3306)
collection.add_singleton(DBContextABC, DBContext)

View File

@@ -1,18 +1,17 @@
from cpl.core.configuration.configuration import Configuration 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.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 from cpl.dependency.service_collection import ServiceCollection
class PostgresModule(Module): class PostgresModule(Module):
dependencies = [] config = [DatabaseSettings]
singleton = [(DBContextABC, DBContext)]
@staticmethod @staticmethod
def register(collection: ServiceCollection): 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)) ServerType.set_server_type(ServerTypes(ServerTypes.POSTGRES.value))
Configuration.set("DB_DEFAULT_PORT", 5432) Configuration.set("DB_DEFAULT_PORT", 5432)
collection.add_singleton(DBContextABC, DBContext)

View File

@@ -0,0 +1,2 @@
from .seeder_service import SeederService
from .migration_service import MigrationService

View File

@@ -7,13 +7,12 @@ from cpl.database.model.migration import Migration
from cpl.database.model.server_type import ServerType, ServerTypes from cpl.database.model.server_type import ServerType, ServerTypes
from cpl.database.schema.executed_migration import ExecutedMigration from cpl.database.schema.executed_migration import ExecutedMigration
from cpl.database.schema.executed_migration_dao import ExecutedMigrationDao 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): class MigrationService(StartupTask):
def __init__(self, logger: DBLogger, db: DBContextABC, executed_migration_dao: ExecutedMigrationDao): def __init__(self, logger: DBLogger, db: DBContextABC, executed_migration_dao: ExecutedMigrationDao):
StartupTask.__init__(self)
self._logger = logger self._logger = logger
self._db = db self._db = db
self._executed_migration_dao = executed_migration_dao 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")) self.with_directory(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../scripts/postgres"))
elif ServerType.server_type == ServerTypes.MYSQL: elif ServerType.server_type == ServerTypes.MYSQL:
self.with_directory(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../scripts/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): async def run(self):
await self._execute(self._load_scripts()) await self._execute(self._load_scripts())
def with_directory(self, directory: str) -> "MigrationService": 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 return self
async def _get_migration_history(self) -> list[ExecutedMigration]: async def _get_migration_history(self) -> list[ExecutedMigration]:

View File

@@ -1,15 +1,17 @@
from cpl.database.abc.data_seeder_abc import DataSeederABC from cpl.database.abc.data_seeder_abc import DataSeederABC
from cpl.database.logger import DBLogger from cpl.database.logger import DBLogger
from cpl.dependency import ServiceProvider from cpl.dependency import ServiceProvider
from cpl.dependency.hosted import StartupTask
class SeederService: class SeederService(StartupTask):
def __init__(self, provider: ServiceProvider): def __init__(self, provider: ServiceProvider):
StartupTask.__init__(self)
self._provider = provider self._provider = provider
self._logger = provider.get_service(DBLogger) self._logger = provider.get_service(DBLogger)
async def seed(self): async def run(self):
seeders = self._provider.get_services(DataSeederABC) seeders = self._provider.get_services(DataSeederABC)
self._logger.debug(f"Found {len(seeders)} seeders") self._logger.debug(f"Found {len(seeders)} seeders")
for seeder in seeders: for seeder in seeders:

View File

@@ -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"): ...

View File

@@ -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"): ...

View File

@@ -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"): ...

View File

@@ -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"): ...

View File

@@ -6,7 +6,7 @@ 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
from cpl.dependency.hosted.startup_task import StartupTask 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_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
@@ -20,10 +20,10 @@ class ServiceCollection:
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[TModule] = set()
@property @property
def loaded_modules(self) -> set[str]: def loaded_modules(self) -> set[TModule]:
return self._loaded_modules return self._loaded_modules
def _check_dependency(self, module: TModule, dependency: TModule | TService, optional: bool = False) -> bool: 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__) module_dependency_error(module.__name__, dependency.__name__)
if dependency.__name__ not in self._loaded_modules: if dependency not in self._loaded_modules:
if optional: if optional:
return False return False
@@ -50,6 +50,63 @@ class ServiceCollection:
return True 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): def _check_dependencies(self, module: TModule):
dependencies: list[TModule | Type] = getattr(module, "dependencies", []) dependencies: list[TModule | Type] = getattr(module, "dependencies", [])
for dependency in dependencies: for dependency in dependencies:
@@ -83,7 +140,7 @@ 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( 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: if service is not None:
self._add_descriptor(service, lifetime, service_type) self._add_descriptor(service, lifetime, service_type)
@@ -120,16 +177,16 @@ class ServiceCollection:
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__
if module in self._modules: 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._check_dependencies(module)
self._add_module_configuration(module)
self._add_module_services(module)
module.register(self)
module().register(self) if module not in self._loaded_modules:
self._loaded_modules.add(module)
if name not in self._loaded_modules:
self._loaded_modules.add(name)
return self return self

View File

@@ -24,7 +24,9 @@ class ServiceProvider:
for descriptor in self._service_descriptors: for descriptor in self._service_descriptors:
if typing.get_origin(service_type) is None and ( 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 return descriptor

View File

@@ -1,9 +1,12 @@
from typing import Type from typing import Type
from cpl.core.configuration import ConfigurationModelABC
from cpl.core.typing import T from cpl.core.typing import T
from cpl.dependency.hosted import StartupTask from cpl.dependency.hosted import StartupTask
from cpl.dependency.module import Module from cpl.dependency.module.module import Module
TModule = Type[Module] TModule = Type[Module]
Modules = set[TModule]
TService = Type[T] TService = Type[T]
TConfig = Type[ConfigurationModelABC]
TStartupTask = Type[StartupTask] TStartupTask = Type[StartupTask]

View File

@@ -1,13 +1,8 @@
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.module.module import Module
from cpl.dependency.module import Module, TModule
from cpl.mail.abc.email_client_abc import EMailClientABC
from cpl.mail.email_client import EMailClient
class MailModule(Module): class MailModule(Module):
dependencies = [] singleton = [(EMailClientABC, EMailClient)]
@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)

View File

@@ -1,13 +1,7 @@
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.module.module import Module
from cpl.dependency.module import Module from cpl.translation.translation_service import TranslationService
from cpl.translation.translation_service_abc import TranslationServiceABC from cpl.translation.translation_service_abc import TranslationServiceABC
class TranslationModule(Module): class TranslationModule(Module):
dependencies = [] singleton = [(TranslationServiceABC, TranslationService)]
@staticmethod
def register(collection: ServiceCollection):
from cpl.translation.translation_service import TranslationService
collection.add_singleton(TranslationServiceABC, TranslationService)