Compare commits

...

4 Commits

Author SHA1 Message Date
56a16cbeba Module dependencies as static var
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
Build on push / prepare (push) Successful in 10s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 20s
Build on push / dependency (push) Successful in 17s
Build on push / application (push) Successful in 15s
Build on push / database (push) Successful in 16s
Build on push / mail (push) Successful in 18s
Build on push / translation (push) Successful in 22s
Build on push / auth (push) Successful in 18s
Build on push / api (push) Successful in 17s
2025-09-26 08:46:30 +02:00
d05d947d54 Import cleanup
All checks were successful
Test before pr merge / test-lint (pull_request) Successful in 6s
Build on push / prepare (push) Successful in 12s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 17s
Build on push / dependency (push) Successful in 17s
Build on push / application (push) Successful in 15s
Build on push / translation (push) Successful in 18s
Build on push / mail (push) Successful in 18s
Build on push / database (push) Successful in 20s
Build on push / auth (push) Successful in 17s
Build on push / api (push) Successful in 13s
2025-09-26 00:03:12 +02:00
0529269747 Fixed formatting
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 19s
Build on push / query (push) Successful in 19s
Build on push / dependency (push) Successful in 14s
Build on push / mail (push) Successful in 16s
Build on push / application (push) Successful in 19s
Build on push / database (push) Successful in 20s
Build on push / translation (push) Successful in 20s
Build on push / auth (push) Successful in 14s
Build on push / api (push) Successful in 15s
2025-09-25 10:37:29 +02:00
e3e1703ff8 Fixed versioning
Some checks failed
Test before pr merge / test-lint (pull_request) Failing after 6s
Build on push / prepare (push) Successful in 9s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 19s
Build on push / dependency (push) Successful in 14s
Build on push / api (push) Has been cancelled
Build on push / auth (push) Has been cancelled
Build on push / application (push) Has been cancelled
Build on push / mail (push) Has been cancelled
Build on push / translation (push) Has been cancelled
Build on push / database (push) Has been cancelled
2025-09-25 10:36:36 +02:00
35 changed files with 212 additions and 167 deletions

View File

@@ -25,7 +25,11 @@ jobs:
git tag git tag
DATE=$(date +'%Y.%m.%d') DATE=$(date +'%Y.%m.%d')
TAG_COUNT=$(git tag -l "${DATE}.*" | wc -l) TAG_COUNT=$(git tag -l "${DATE}.*" | wc -l)
BUILD_NUMBER=$(($TAG_COUNT + 1)) if [ "$TAG_COUNT" -eq 0 ]; then
BUILD_NUMBER=0
else
BUILD_NUMBER=$(($TAG_COUNT + 1))
fi
VERSION_SUFFIX=${{ inputs.version_suffix }} VERSION_SUFFIX=${{ inputs.version_suffix }}
if [ -n "$VERSION_SUFFIX" ] && [ "$VERSION_SUFFIX" = "dev" ]; then if [ -n "$VERSION_SUFFIX" ] && [ "$VERSION_SUFFIX" = "dev" ]; then

View File

@@ -1,9 +1,9 @@
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from cpl import api 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.api_module import ApiModule from cpl.application.application_builder import ApplicationBuilder
from cpl.application 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
@@ -40,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,4 +1,4 @@
from .error import APIError, AlreadyExists, EndpointNotImplemented, Forbidden, NotFound, Unauthorized from .error import APIError, AlreadyExists, EndpointNotImplemented, Forbidden, NotFound, Unauthorized
from .logger import APILogger from .logger import APILogger
from .settings import ApiSettings from .settings import ApiSettings
from .api_module import ApiModule

View File

@@ -0,0 +1,21 @@
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
class ApiModule(Module):
dependencies = []
@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)

View File

@@ -25,7 +25,7 @@ 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_module import ApiModule 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

View File

@@ -1,26 +0,0 @@
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.core.errors import dependency_error
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.dependency.module import Module, TModule
class ApiModule(Module):
@staticmethod
def dependencies() -> list[TModule]:
return [AuthModule, DatabaseModule, PermissionsModule]
@staticmethod
def register(collection: "ServiceCollection"):
collection.add_module(DatabaseModule)
collection.add_module(AuthModule)
collection.add_module(PermissionsModule)
collection.add_singleton(PolicyRegistry)
collection.add_singleton(RouteRegistry)

View File

@@ -1,2 +1,2 @@
from .application_builder import ApplicationBuilder from .application_builder import ApplicationBuilder
from .host import Host from .host import Host

View File

@@ -77,4 +77,4 @@ class Host:
if asyncio.iscoroutinefunction(func): if asyncio.iscoroutinefunction(func):
return cls.get_loop().run_until_complete(func(*args, **kwargs)) return cls.get_loop().run_until_complete(func(*args, **kwargs))
return func(*args, **kwargs) return func(*args, **kwargs)

View File

@@ -7,6 +7,7 @@ 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 .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: def _with_permissions(self: _ApplicationABC, *permissions: Type[Enum]) -> _ApplicationABC:
@@ -17,5 +18,4 @@ def _with_permissions(self: _ApplicationABC, *permissions: Type[Enum]) -> _Appli
return self return self
_ApplicationABC.extend(_ApplicationABC.with_permissions, _with_permissions) _ApplicationABC.extend(_ApplicationABC.with_permissions, _with_permissions)

View File

@@ -1,30 +1,31 @@
import os import os
from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin as _KeycloakAdmin
from cpl.auth.keycloak.keycloak_client import KeycloakClient as _KeycloakClient
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
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):
@staticmethod dependencies = [DatabaseModule, (MySQLModule, PostgresModule)]
def dependencies() -> list[TModule]:
return [DatabaseModule]
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):
collection.add_singleton(_KeycloakClient) from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin
collection.add_singleton(_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
collection.add_singleton(KeycloakClient)
collection.add_singleton(KeycloakAdmin)
collection.add_singleton(AuthUserDao) collection.add_singleton(AuthUserDao)
collection.add_singleton(ApiKeyDao) collection.add_singleton(ApiKeyDao)

View File

@@ -0,0 +1,4 @@
from .permission_module import PermissionsModule
from .permission_seeder import PermissionSeeder
from .permissions import Permissions
from .permissions_registry import PermissionsRegistry

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

@@ -14,8 +14,9 @@ def dependency_error(src: str, package_name: str, e: ImportError = None) -> None
exit(1) exit(1)
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)
@@ -23,4 +24,4 @@ def module_dependency_error(src: str, module: str, e: ImportError = None) -> Non
elif e is not None: elif e is not None:
Console.write_line("->", str(e)) Console.write_line("->", str(e))
exit(1) exit(1)

View File

@@ -3,7 +3,9 @@ import os
from cpl.application.abc import ApplicationABC as _ApplicationABC 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 .table_manager import TableManager from .table_manager import TableManager
from .logger import DBLogger
def _with_migrations(self: _ApplicationABC, *paths: str | list[str]) -> _ApplicationABC: def _with_migrations(self: _ApplicationABC, *paths: str | list[str]) -> _ApplicationABC:

View File

@@ -1,4 +1,6 @@
from .connection_abc import ConnectionABC from .connection_abc import ConnectionABC
from .data_access_object_abc import DataAccessObjectABC
from .data_seeder_abc import DataSeederABC
from .db_context_abc import DBContextABC from .db_context_abc import DBContextABC
from .db_join_model_abc import DbJoinModelABC from .db_join_model_abc import DbJoinModelABC
from .db_model_abc import DbModelABC from .db_model_abc import DbModelABC

View File

@@ -14,7 +14,7 @@ from cpl.database.logger import DBLogger
from cpl.database.model.server_type import ServerType, ServerTypes from cpl.database.model.server_type import ServerType, ServerTypes
from cpl.database.postgres.sql_select_builder import SQLSelectBuilder from cpl.database.postgres.sql_select_builder import SQLSelectBuilder
from cpl.database.typing import T_DBM, Attribute, AttributeFilters, AttributeSorts from cpl.database.typing import T_DBM, Attribute, AttributeFilters, AttributeSorts
from cpl.dependency import get_provider from cpl.dependency.context import get_provider
class DataAccessObjectABC(ABC, Generic[T_DBM]): class DataAccessObjectABC(ABC, Generic[T_DBM]):

View File

@@ -2,7 +2,7 @@ from abc import abstractmethod
from datetime import datetime from datetime import datetime
from typing import Type from typing import Type
from cpl.database import TableManager from cpl.database.table_manager import TableManager
from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.abc.data_access_object_abc import DataAccessObjectABC
from cpl.database.abc.db_model_abc import DbModelABC from cpl.database.abc.db_model_abc import DbModelABC

View File

@@ -1,22 +1,18 @@
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.database.schema.executed_migration_dao import ExecutedMigrationDao from cpl.dependency.module import Module
from cpl.database.service.migration_service import MigrationService
from cpl.database.service.seeder_service import SeederService
from cpl.dependency.module import Module, TModule
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):
from cpl.database.schema import ExecutedMigrationDao
from cpl.database.service.migration_service import MigrationService
from cpl.database.service.seeder_service import SeederService
collection.add_singleton(ExecutedMigrationDao) collection.add_singleton(ExecutedMigrationDao)
collection.add_singleton(MigrationService) collection.add_singleton(MigrationService)
collection.add_singleton(SeederService) collection.add_singleton(SeederService)

View File

@@ -0,0 +1,4 @@
from .connection import DatabaseConnection
from .db_context import DBContext
from .mysql_module import MySQLModule
from .mysql_pool import MySQLPool

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.server_type import ServerTypes, ServerType from cpl.database.model.server_type import ServerTypes, ServerType
from cpl.database.mysql.db_context import DBContext from cpl.dependency.module import Module
from cpl.dependency.module import Module, TModule
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):
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)

View File

@@ -0,0 +1,4 @@
from .db_context import DBContext
from .postgres_module import PostgresModule
from .postgres_pool import PostgresPool
from .sql_select_builder import SQLSelectBuilder

View File

@@ -1,19 +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.database_module import DatabaseModule
from cpl.database.model.server_type import ServerTypes, ServerType from cpl.database.model.server_type import ServerTypes, ServerType
from cpl.database.postgres.db_context import DBContext from cpl.dependency.module import Module
from cpl.dependency.module import Module, TModule
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):
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)

View File

@@ -0,0 +1,2 @@
from .executed_migration import ExecutedMigration
from .executed_migration_dao import ExecutedMigrationDao

View File

@@ -1,4 +1,4 @@
from cpl.database import TableManager from cpl.database.table_manager import TableManager
from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.abc.data_access_object_abc import DataAccessObjectABC
from cpl.database.schema.executed_migration import ExecutedMigration from cpl.database.schema.executed_migration import ExecutedMigration

View File

@@ -1,9 +1,9 @@
import glob import glob
import os import os
from cpl.database.abc import DBContextABC from cpl.database.abc.db_context_abc import DBContextABC
from cpl.database.logger import DBLogger from cpl.database.logger import DBLogger
from cpl.database.model import Migration 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

View File

@@ -1,2 +1,2 @@
from .hosted_service import HostedService from .hosted_service import HostedService
from .startup_task import StartupTask from .startup_task import StartupTask

View File

@@ -6,4 +6,4 @@ class HostedService(ABC):
async def start(self): ... async def start(self): ...
@abstractmethod @abstractmethod
async def stop(self): ... async def stop(self): ...

View File

@@ -1,13 +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

@@ -23,7 +23,9 @@ class ServiceProvider:
type_args = list(typing.get_args(service_type)) type_args = list(typing.get_args(service_type))
for descriptor in self._service_descriptors: 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)): if typing.get_origin(service_type) is None and (
descriptor.service_type == service_type or issubclass(descriptor.base_type, service_type)
):
return descriptor return descriptor
descriptor_base_type = typing.get_origin(descriptor.base_type) or descriptor.base_type descriptor_base_type = typing.get_origin(descriptor.base_type) or descriptor.base_type
@@ -65,9 +67,7 @@ class ServiceProvider:
implementations.append(descriptor.implementation) implementations.append(descriptor.implementation)
continue continue
implementation = self._build_service( implementation = self._build_service(descriptor, *args, origin_service_type=service_type, **kwargs)
descriptor, *args, origin_service_type=service_type, **kwargs
)
if descriptor.lifetime in (ServiceLifetimeEnum.singleton, ServiceLifetimeEnum.scoped): if descriptor.lifetime in (ServiceLifetimeEnum.singleton, ServiceLifetimeEnum.scoped):
descriptor.implementation = implementation descriptor.implementation = implementation
@@ -81,7 +81,9 @@ class ServiceProvider:
parameter = param[1] parameter = param[1]
if parameter.name != "self" and parameter.annotation != Parameter.empty: if parameter.name != "self" and parameter.annotation != Parameter.empty:
if typing.get_origin(parameter.annotation) == list: if typing.get_origin(parameter.annotation) == list:
params.append(self._get_services(typing.get_args(parameter.annotation)[0], service_type=origin_service_type)) params.append(
self._get_services(typing.get_args(parameter.annotation)[0], service_type=origin_service_type)
)
elif parameter.annotation == Source: elif parameter.annotation == Source:
params.append(origin_service_type.__name__) params.append(origin_service_type.__name__)
@@ -104,7 +106,9 @@ class ServiceProvider:
return params return params
def _build_service(self, descriptor: ServiceDescriptor, *args, origin_service_type: type = None, **kwargs) -> object: def _build_service(
self, descriptor: ServiceDescriptor, *args, origin_service_type: type = None, **kwargs
) -> object:
if descriptor.implementation is not None: if descriptor.implementation is not None:
service_type = type(descriptor.implementation) service_type = type(descriptor.implementation)
else: else:
@@ -131,7 +135,11 @@ class ServiceProvider:
yield scoped_provider yield scoped_provider
def get_hosted_services(self) -> list[Optional[T]]: def get_hosted_services(self) -> list[Optional[T]]:
hosted_services = [self.get_service(d.service_type) for d in self._service_descriptors if d.lifetime == ServiceLifetimeEnum.hosted] hosted_services = [
self.get_service(d.service_type)
for d in self._service_descriptors
if d.lifetime == ServiceLifetimeEnum.hosted
]
return hosted_services return hosted_services
def get_service(self, service_type: Type[T], *args, **kwargs) -> Optional[T]: def get_service(self, service_type: Type[T], *args, **kwargs) -> Optional[T]:

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,20 +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 .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,15 +1,13 @@
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
from cpl.mail.email_client import EMailClient
from cpl.mail.abc.email_client_abc import EMailClientABC
class MailModule(Module): class MailModule(Module):
@staticmethod dependencies = []
def dependencies() -> list[TModule]:
return []
@staticmethod @staticmethod
def register(collection: ServiceCollection): def register(collection: ServiceCollection):
collection.add_singleton(EMailClientABC, EMailClient) from cpl.mail.abc.email_client_abc import EMailClientABC
from cpl.mail.email_client import EMailClient
collection.add_singleton(EMailClientABC, EMailClient)

View File

@@ -1,22 +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_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,14 +1,13 @@
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 import TranslationService
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):
from cpl.translation.translation_service import TranslationService
collection.add_singleton(TranslationServiceABC, TranslationService) collection.add_singleton(TranslationServiceABC, TranslationService)