WIP: dev into master #184
@@ -0,0 +1,20 @@
|
|||||||
|
from cpl.dependency.service_collection import ServiceCollection as _ServiceCollection
|
||||||
|
|
||||||
|
def add_api(collection: _ServiceCollection):
|
||||||
|
try:
|
||||||
|
from cpl.database import mysql
|
||||||
|
collection.add_module(mysql)
|
||||||
|
except ImportError as e:
|
||||||
|
from cpl.core.errors import dependency_error
|
||||||
|
dependency_error("cpl-database", e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cpl import auth
|
||||||
|
from cpl.auth import permission
|
||||||
|
collection.add_module(auth)
|
||||||
|
collection.add_module(permission)
|
||||||
|
except ImportError as e:
|
||||||
|
from cpl.core.errors import dependency_error
|
||||||
|
dependency_error("cpl-auth", e)
|
||||||
|
|
||||||
|
_ServiceCollection.with_module(add_api, __name__)
|
||||||
@@ -10,6 +10,7 @@ from starlette.responses import JSONResponse
|
|||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
from starlette.types import ExceptionHandler
|
from starlette.types import ExceptionHandler
|
||||||
|
|
||||||
|
from cpl import api, auth
|
||||||
from cpl.api.api_logger import APILogger
|
from cpl.api.api_logger import APILogger
|
||||||
from cpl.api.api_settings import ApiSettings
|
from cpl.api.api_settings import ApiSettings
|
||||||
from cpl.api.error import APIError
|
from cpl.api.error import APIError
|
||||||
@@ -27,7 +28,7 @@ _logger = APILogger("API")
|
|||||||
|
|
||||||
class WebApp(ApplicationABC):
|
class WebApp(ApplicationABC):
|
||||||
def __init__(self, services: ServiceProviderABC):
|
def __init__(self, services: ServiceProviderABC):
|
||||||
super().__init__(services)
|
super().__init__(services, [auth, api])
|
||||||
self._app: Starlette | None = None
|
self._app: Starlette | None = None
|
||||||
|
|
||||||
self._api_settings = Configuration.get(ApiSettings)
|
self._api_settings = Configuration.get(ApiSettings)
|
||||||
|
|||||||
@@ -22,8 +22,16 @@ class ApplicationABC(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __init__(self, services: ServiceProviderABC):
|
def __init__(self, services: ServiceProviderABC, required_modules: list[str | object] = None):
|
||||||
self._services = services
|
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]):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from cpl.application.abc.application_extension_abc import ApplicationExtensionAB
|
|||||||
from cpl.application.abc.startup_abc import StartupABC
|
from cpl.application.abc.startup_abc import StartupABC
|
||||||
from cpl.application.abc.startup_extension_abc import StartupExtensionABC
|
from cpl.application.abc.startup_extension_abc import StartupExtensionABC
|
||||||
from cpl.application.host import Host
|
from cpl.application.host import Host
|
||||||
|
from cpl.core.errors import dependency_error
|
||||||
from cpl.dependency.service_collection import ServiceCollection
|
from cpl.dependency.service_collection import ServiceCollection
|
||||||
|
|
||||||
TApp = TypeVar("TApp", bound=ApplicationABC)
|
TApp = TypeVar("TApp", bound=ApplicationABC)
|
||||||
@@ -35,6 +36,18 @@ class ApplicationBuilder(Generic[TApp]):
|
|||||||
def service_provider(self):
|
def service_provider(self):
|
||||||
return self._services.build()
|
return self._services.build()
|
||||||
|
|
||||||
|
def validate_app_required_modules(self, app: ApplicationABC):
|
||||||
|
for module in app.required_modules:
|
||||||
|
if module in self._services.loaded_modules:
|
||||||
|
continue
|
||||||
|
|
||||||
|
dependency_error(
|
||||||
|
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
|
||||||
@@ -62,4 +75,6 @@ class ApplicationBuilder(Generic[TApp]):
|
|||||||
for extension in self._app_extensions:
|
for extension in self._app_extensions:
|
||||||
Host.run(extension.run, self.service_provider)
|
Host.run(extension.run, self.service_provider)
|
||||||
|
|
||||||
return self._app(self.service_provider)
|
app = self._app(self.service_provider)
|
||||||
|
self.validate_app_required_modules(app)
|
||||||
|
return app
|
||||||
|
|||||||
@@ -40,11 +40,10 @@ def _add_daos(collection: _ServiceCollection):
|
|||||||
def add_auth(collection: _ServiceCollection):
|
def add_auth(collection: _ServiceCollection):
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from cpl.core.console import Console
|
|
||||||
from cpl.database.service.migration_service import MigrationService
|
|
||||||
from cpl.database.model.server_type import ServerType, ServerTypes
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from cpl.database.service.migration_service import MigrationService
|
||||||
|
from cpl.database.model.server_type import ServerType, ServerTypes
|
||||||
|
|
||||||
collection.add_singleton(_KeycloakClient)
|
collection.add_singleton(_KeycloakClient)
|
||||||
collection.add_singleton(_KeycloakAdmin)
|
collection.add_singleton(_KeycloakAdmin)
|
||||||
|
|
||||||
@@ -59,22 +58,23 @@ def add_auth(collection: _ServiceCollection):
|
|||||||
elif ServerType.server_type == ServerTypes.MYSQL:
|
elif ServerType.server_type == ServerTypes.MYSQL:
|
||||||
migration_service.with_directory(os.path.join(os.path.dirname(os.path.realpath(__file__)), "scripts/mysql"))
|
migration_service.with_directory(os.path.join(os.path.dirname(os.path.realpath(__file__)), "scripts/mysql"))
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
Console.error("cpl-auth is not installed", str(e))
|
from cpl.core.console import Console
|
||||||
|
Console.error("cpl-database is not installed", str(e))
|
||||||
|
|
||||||
|
|
||||||
def add_permission(collection: _ServiceCollection):
|
def add_permission(collection: _ServiceCollection):
|
||||||
from cpl.auth.permission_seeder import PermissionSeeder
|
from .permission_seeder import PermissionSeeder
|
||||||
from cpl.database.abc.data_seeder_abc import DataSeederABC
|
from .permission.permissions_registry import PermissionsRegistry
|
||||||
from cpl.auth.permission.permissions_registry import PermissionsRegistry
|
from .permission.permissions import Permissions
|
||||||
from cpl.auth.permission.permissions import Permissions
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from cpl.database.abc.data_seeder_abc import DataSeederABC
|
||||||
collection.add_singleton(DataSeederABC, PermissionSeeder)
|
collection.add_singleton(DataSeederABC, PermissionSeeder)
|
||||||
PermissionsRegistry.with_enum(Permissions)
|
PermissionsRegistry.with_enum(Permissions)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
from cpl.core.console import Console
|
from cpl.core.console import Console
|
||||||
|
|
||||||
Console.error("cpl-auth is not installed", str(e))
|
Console.error("cpl-database is not installed", str(e))
|
||||||
|
|
||||||
|
|
||||||
_ServiceCollection.with_module(add_auth, __name__)
|
_ServiceCollection.with_module(add_auth, __name__)
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class Configuration:
|
|||||||
key_name = key.__name__ if inspect.isclass(key) else key
|
key_name = key.__name__ if inspect.isclass(key) else key
|
||||||
|
|
||||||
result = cls._config.get(key_name, default)
|
result = cls._config.get(key_name, default)
|
||||||
if issubclass(key, ConfigurationModelABC) and result == default:
|
if isclass(key) and issubclass(key, ConfigurationModelABC) and result == default:
|
||||||
result = key()
|
result = key()
|
||||||
cls.set(key, result)
|
cls.set(key, result)
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class ConfigurationModelABC(ABC):
|
|||||||
value = cast(Environment.get(env_field, str), cast_type)
|
value = cast(Environment.get(env_field, str), cast_type)
|
||||||
|
|
||||||
if value is None and required:
|
if value is None and required:
|
||||||
raise ValueError(f"{field} is required")
|
raise ValueError(f"{type(self).__name__}.{field} is required")
|
||||||
elif value is None:
|
elif value is None:
|
||||||
self._options[field] = default
|
self._options[field] = default
|
||||||
return
|
return
|
||||||
|
|||||||
15
src/cpl-core/cpl/core/errors.py
Normal file
15
src/cpl-core/cpl/core/errors.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from cpl.core.console import Console
|
||||||
|
|
||||||
|
|
||||||
|
def dependency_error(package_name: str, e: ImportError) -> None:
|
||||||
|
Console.error(f"'{package_name}' is required to use this feature. Please install it and try again.")
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
if not tb.startswith("NoneType: None"):
|
||||||
|
Console.write_line("->", tb)
|
||||||
|
|
||||||
|
elif e is not None:
|
||||||
|
Console.write_line("->", str(e))
|
||||||
|
|
||||||
|
exit(1)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Union, Type, Callable
|
from typing import Union, Type, Callable, Self
|
||||||
|
|
||||||
from cpl.core.log.logger import Logger
|
from cpl.core.log.logger import Logger
|
||||||
from cpl.core.log.logger_abc import LoggerABC
|
from cpl.core.log.logger_abc import LoggerABC
|
||||||
@@ -15,12 +15,17 @@ class ServiceCollection:
|
|||||||
_modules: dict[str, Callable] = {}
|
_modules: dict[str, Callable] = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def with_module(cls, func: Callable, name: str = None):
|
def with_module(cls, func: Callable, name: str = None) -> type[Self]:
|
||||||
cls._modules[func.__name__ if name is None else name] = func
|
cls._modules[func.__name__ if name is None else name] = func
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._service_descriptors: list[ServiceDescriptor] = []
|
self._service_descriptors: list[ServiceDescriptor] = []
|
||||||
|
self._loaded_modules: set[str] = set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loaded_modules(self) -> set[str]:
|
||||||
|
return self._loaded_modules
|
||||||
|
|
||||||
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
|
||||||
@@ -45,15 +50,15 @@ class ServiceCollection:
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_singleton(self, service_type: T, service: Service = None):
|
def add_singleton(self, service_type: 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):
|
def add_scoped(self, service_type: 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):
|
def add_transient(self, service_type: 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
|
||||||
|
|
||||||
@@ -62,7 +67,7 @@ class ServiceCollection:
|
|||||||
ServiceProviderABC.set_global_provider(sp)
|
ServiceProviderABC.set_global_provider(sp)
|
||||||
return sp
|
return sp
|
||||||
|
|
||||||
def add_module(self, module: str | object):
|
def add_module(self, module: str | object) -> Self:
|
||||||
if not isinstance(module, str):
|
if not isinstance(module, str):
|
||||||
module = module.__name__
|
module = module.__name__
|
||||||
|
|
||||||
@@ -70,7 +75,10 @@ class ServiceCollection:
|
|||||||
raise ValueError(f"Module {module} not found")
|
raise ValueError(f"Module {module} not found")
|
||||||
|
|
||||||
self._modules[module](self)
|
self._modules[module](self)
|
||||||
|
if module not in self._loaded_modules:
|
||||||
|
self._loaded_modules.add(module)
|
||||||
|
return self
|
||||||
|
|
||||||
def add_logging(self):
|
def add_logging(self) -> Self:
|
||||||
self.add_transient(LoggerABC, Logger)
|
self.add_transient(LoggerABC, Logger)
|
||||||
return self
|
return self
|
||||||
|
|||||||
8
tests/custom/api/src/appsettings.development.json
Normal file
8
tests/custom/api/src/appsettings.development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "log_$start_time.log",
|
||||||
|
"ConsoleLevel": "TRACE",
|
||||||
|
"Level": "TRACE"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
tests/custom/api/src/appsettings.edrafts-pc.json
Normal file
26
tests/custom/api/src/appsettings.edrafts-pc.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"TimeFormat": {
|
||||||
|
"DateFormat": "%Y-%m-%d",
|
||||||
|
"TimeFormat": "%H:%M:%S",
|
||||||
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Log": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "log_$start_time.log",
|
||||||
|
"ConsoleLevel": "TRACE",
|
||||||
|
"Level": "TRACE"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Database": {
|
||||||
|
"Host": "localhost",
|
||||||
|
"User": "cpl",
|
||||||
|
"Port": 3306,
|
||||||
|
"Password": "cpl",
|
||||||
|
"Database": "cpl",
|
||||||
|
"Charset": "utf8mb4",
|
||||||
|
"UseUnicode": "true",
|
||||||
|
"Buffered": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
tests/custom/api/src/appsettings.json
Normal file
15
tests/custom/api/src/appsettings.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"TimeFormat": {
|
||||||
|
"DateFormat": "%Y-%m-%d",
|
||||||
|
"TimeFormat": "%H:%M:%S",
|
||||||
|
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Log": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "log_$start_time.log",
|
||||||
|
"ConsoleLevel": "ERROR",
|
||||||
|
"Level": "WARNING"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
from cpl import api
|
||||||
from cpl.api.web_app import WebApp
|
from cpl.api.web_app import WebApp
|
||||||
from cpl.application import ApplicationBuilder
|
from cpl.application import ApplicationBuilder
|
||||||
|
from cpl.core.configuration import Configuration
|
||||||
|
from cpl.core.environment import Environment
|
||||||
from service import PingService
|
from service import PingService
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
builder = ApplicationBuilder[WebApp](WebApp)
|
builder = ApplicationBuilder[WebApp](WebApp)
|
||||||
|
|
||||||
|
|
||||||
|
Configuration.add_json_file(f"appsettings.json")
|
||||||
|
Configuration.add_json_file(f"appsettings.{Environment.get_environment()}.json")
|
||||||
|
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
|
||||||
|
|
||||||
builder.services.add_logging()
|
builder.services.add_logging()
|
||||||
builder.services.add_transient(PingService)
|
builder.services.add_transient(PingService)
|
||||||
|
builder.services.add_module(api)
|
||||||
|
|
||||||
app = builder.build()
|
app = builder.build()
|
||||||
app.with_route(path="/route1", fn=lambda r: JSONResponse("route1"), method="GET")
|
app.with_route(path="/route1", fn=lambda r: JSONResponse("route1"), method="GET")
|
||||||
|
|||||||
Reference in New Issue
Block a user