Compare commits

..

9 Commits

Author SHA1 Message Date
4f698269b5 Fixed api build
All checks were successful
Build on push / prepare (push) Successful in 9s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 17s
Build on push / dependency (push) Successful in 18s
Build on push / application (push) Successful in 15s
Build on push / database (push) Successful in 18s
Build on push / translation (push) Successful in 18s
Build on push / mail (push) Successful in 19s
Build on push / auth (push) Successful in 15s
Build on push / api (push) Successful in 14s
2025-09-19 21:12:33 +02:00
ddc62dfb9a Added api & route handling
Some checks failed
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 17s
Build on push / application (push) Successful in 15s
Build on push / database (push) Successful in 18s
Build on push / mail (push) Successful in 19s
Build on push / translation (push) Successful in 23s
Build on push / auth (push) Successful in 16s
Build on push / api (push) Failing after 14s
2025-09-19 21:03:33 +02:00
1a67318091 Config model options handling. Closes #185
All checks were successful
Build on push / prepare (push) Successful in 10s
Build on push / core (push) Successful in 19s
Build on push / query (push) Successful in 19s
Build on push / dependency (push) Successful in 25s
Build on push / translation (push) Successful in 17s
Build on push / database (push) Successful in 20s
Build on push / application (push) Successful in 21s
Build on push / mail (push) Successful in 20s
Build on push / auth (push) Successful in 14s
2025-09-19 17:47:49 +02:00
2be58f6577 Introduced fernet to credential manager. Closes #183
All checks were successful
Build on push / prepare (push) Successful in 10s
Build on push / core (push) Successful in 19s
Build on push / query (push) Successful in 22s
Build on push / dependency (push) Successful in 15s
Build on push / application (push) Successful in 20s
Build on push / database (push) Successful in 21s
Build on push / translation (push) Successful in 21s
Build on push / mail (push) Successful in 22s
Build on push / auth (push) Successful in 18s
2025-09-19 15:01:16 +02:00
9c6078f4fd with_logging & logger level fix
All checks were successful
Build on push / prepare (push) Successful in 8s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 18s
Build on push / dependency (push) Successful in 14s
Build on push / translation (push) Successful in 15s
Build on push / database (push) Successful in 17s
Build on push / application (push) Successful in 18s
Build on push / mail (push) Successful in 20s
Build on push / auth (push) Successful in 18s
2025-09-17 22:18:38 +02:00
dfdc31512d App with extension functions
All checks were successful
Build on push / prepare (push) Successful in 9s
Build on push / core (push) Successful in 17s
Build on push / query (push) Successful in 17s
Build on push / dependency (push) Successful in 14s
Build on push / translation (push) Successful in 14s
Build on push / database (push) Successful in 18s
Build on push / mail (push) Successful in 19s
Build on push / application (push) Successful in 22s
Build on push / auth (push) Successful in 14s
2025-09-17 21:56:47 +02:00
ab7ff7da93 Made startup/app extensions static 2025-09-17 20:54:21 +02:00
41087a838b Removed pass from empty functions
All checks were successful
Build on push / prepare (push) Successful in 9s
Build on push / core (push) Successful in 17s
Build on push / query (push) Successful in 17s
Build on push / dependency (push) Successful in 17s
Build on push / translation (push) Successful in 14s
Build on push / mail (push) Successful in 18s
Build on push / database (push) Successful in 18s
Build on push / application (push) Successful in 24s
Build on push / auth (push) Successful in 14s
2025-09-17 20:49:15 +02:00
836b92ccbf Further console test 2025-09-17 20:44:25 +02:00
113 changed files with 1151 additions and 737 deletions

View File

@@ -12,6 +12,13 @@ jobs:
version_suffix: 'dev' version_suffix: 'dev'
secrets: inherit secrets: inherit
api:
uses: ./.gitea/workflows/package.yaml
needs: [ prepare, application, auth, core, dependency ]
with:
working_directory: src/cpl-api
secrets: inherit
application: application:
uses: ./.gitea/workflows/package.yaml uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core, dependency ] needs: [ prepare, core, dependency ]

1
.gitignore vendored
View File

@@ -113,6 +113,7 @@ venv.bak/
# Custom Environments # Custom Environments
cpl-env/ cpl-env/
.secret
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject

View File

@@ -0,0 +1,7 @@
from cpl.core.log.logger import Logger
class APILogger(Logger):
def __init__(self, source: str):
Logger.__init__(self, source, "api")

View File

@@ -0,0 +1,13 @@
from typing import Optional
from cpl.core.configuration import ConfigurationModelABC
class ApiSettings(ConfigurationModelABC):
def __init__(self, src: Optional[dict] = None):
super().__init__(src)
self.option("host", str, "0.0.0.0")
self.option("port", int, 5000)
self.option("allowed_origins", list[str])

View File

@@ -0,0 +1,25 @@
from http.client import HTTPException
class APIError(HTTPException):
status_code = 500
class Unauthorized(APIError):
status_code = 401
class Forbidden(APIError):
status_code = 403
class NotFound(APIError):
status_code = 404
class AlreadyExists(APIError):
status_code = 409
class EndpointNotImplemented(APIError):
status_code = 501

View File

@@ -0,0 +1,65 @@
import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from cpl.api.api_logger import APILogger
_logger = APILogger(__name__)
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
await self._log_request(request)
response = await call_next(request)
await self._log_after_request(request, response)
return response
@staticmethod
def _filter_relevant_headers(headers: dict) -> dict:
relevant_keys = {
"content-type",
"host",
"connection",
"user-agent",
"origin",
"referer",
"accept",
}
return {key: value for key, value in headers.items() if key in relevant_keys}
@classmethod
async def _log_request(cls, request: Request):
_logger.debug(
f"Request {request.state.request_id}: {request.method}@{request.url.path} from {request.client.host}"
)
from cpl.core.ctx.user_context import get_user
user = get_user()
request_info = {
"headers": cls._filter_relevant_headers(dict(request.headers)),
"args": dict(request.query_params),
"form-data": (
await request.form()
if request.headers.get("content-type") == "application/x-www-form-urlencoded"
else None
),
"payload": (await request.json() if request.headers.get("content-length") == "0" else None),
"user": f"{user.id}-{user.keycloak_id}" if user else None,
"files": (
{key: file.filename for key, file in (await request.form()).items()} if await request.form() else None
),
}
_logger.trace(f"Request {request.state.request_id}: {request_info}")
@staticmethod
async def _log_after_request(request: Request, response: Response):
duration = (time.time() - request.state.start_time) * 1000
_logger.info(
f"Request finished {request.state.request_id}: {response.status_code}-{request.method}@{request.url.path} from {request.client.host} in {duration:.2f}ms"
)

View File

@@ -0,0 +1,48 @@
import time
from contextvars import ContextVar
from typing import Optional, Union
from uuid import uuid4
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.websockets import WebSocket
from cpl.api.api_logger import APILogger
from cpl.api.typing import TRequest
_request_context: ContextVar[Union[TRequest, None]] = ContextVar("request", default=None)
_logger = APILogger(__name__)
class RequestMiddleware(BaseHTTPMiddleware):
_request_token = {}
_user_token = {}
@classmethod
async def set_request_data(cls, request: TRequest):
request.state.request_id = uuid4()
request.state.start_time = time.time()
_logger.trace(f"Set new current request: {request.state.request_id}")
cls._request_token[request.state.request_id] = _request_context.set(request)
@classmethod
async def clean_request_data(cls):
request = get_request()
if request is None:
return
if request.state.request_id in cls._request_token:
_request_context.reset(cls._request_token[request.state.request_id])
async def dispatch(self, request: TRequest, call_next):
await self.set_request_data(request)
try:
response = await call_next(request)
return response
finally:
await self.clean_request_data()
def get_request() -> Optional[Union[TRequest, WebSocket]]:
return _request_context.get()

View File

@@ -0,0 +1,60 @@
from starlette.routing import Route
class Router:
_registered_routes: list[Route] = []
@classmethod
def get_routes(cls) -> list[Route]:
return cls._registered_routes
@classmethod
def route(cls, path=None, **kwargs):
def inner(fn):
cls._registered_routes.append(Route(path, fn, **kwargs))
setattr(fn, "_route_path", path)
return fn
return inner
@classmethod
def get(cls, path=None, **kwargs):
return cls.route(path, methods=["GET"], **kwargs)
@classmethod
def post(cls, path=None, **kwargs):
return cls.route(path, methods=["POST"], **kwargs)
@classmethod
def head(cls, path=None, **kwargs):
return cls.route(path, methods=["HEAD"], **kwargs)
@classmethod
def put(cls, path=None, **kwargs):
return cls.route(path, methods=["PUT"], **kwargs)
@classmethod
def delete(cls, path=None, **kwargs):
return cls.route(path, methods=["DELETE"], **kwargs)
@classmethod
def override(cls):
"""
Decorator to override an existing route with the same path.
Usage:
@Route.override()
@Route.get("/example")
async def example_endpoint(request: TRequest):
...
"""
def inner(fn):
route_path = getattr(fn, "_route_path", None)
routes = list(filter(lambda x: x.path == route_path, cls._registered_routes))
for route in routes[:-1]:
cls._registered_routes.remove(route)
return fn
return inner

View File

@@ -0,0 +1,13 @@
from typing import Union, Literal, Callable
from urllib.request import Request
from starlette.middleware import Middleware
from starlette.types import ASGIApp
from starlette.websockets import WebSocket
TRequest = Union[Request, WebSocket]
HTTPMethods = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
PartialMiddleware = Union[
Middleware,
Callable[[ASGIApp], ASGIApp],
]

View File

@@ -0,0 +1,153 @@
import os
from typing import Mapping, Any, Callable
import uvicorn
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.types import ExceptionHandler
from cpl.api.api_logger import APILogger
from cpl.api.api_settings import ApiSettings
from cpl.api.error import APIError
from cpl.api.middleware.logging import LoggingMiddleware
from cpl.api.middleware.request import RequestMiddleware
from cpl.api.router import Router
from cpl.api.typing import HTTPMethods, PartialMiddleware
from cpl.application.abc.application_abc import ApplicationABC
from cpl.core.configuration import Configuration
from cpl.dependency.service_provider_abc import ServiceProviderABC
_logger = APILogger("API")
class WebApp(ApplicationABC):
def __init__(self, services: ServiceProviderABC):
super().__init__(services)
self._app: Starlette | None = None
self._api_settings = Configuration.get(ApiSettings)
self._routes: list[Route] = []
self._middleware: list[Middleware] = [
Middleware(RequestMiddleware),
Middleware(LoggingMiddleware),
]
self._exception_handlers: Mapping[Any, ExceptionHandler] = {Exception: self.handle_exception}
@staticmethod
async def handle_exception(request: Request, exc: Exception):
if hasattr(request.state, "request_id"):
_logger.error(f"Request {request.state.request_id}", exc)
else:
_logger.error("Request unknown", exc)
if isinstance(exc, APIError):
return JSONResponse({"error": str(exc)}, status_code=exc.status_code)
return JSONResponse({"error": str(exc)}, status_code=500)
def _get_allowed_origins(self):
origins = self._api_settings.allowed_origins
if origins is None or origins == "":
_logger.warning("No allowed origins specified, allowing all origins")
return ["*"]
_logger.debug(f"Allowed origins: {origins}")
return origins.split(",")
def with_app(self, app: Starlette):
assert app is not None, "app must not be None"
assert isinstance(app, Starlette), "app must be an instance of Starlette"
self._app = app
return self
def _check_for_app(self):
if self._app is not None:
raise ValueError("App is already set, cannot add routes or middleware")
def with_routes_directory(self, directory: str) -> "WebApp":
self._check_for_app()
assert directory is not None, "directory must not be None"
base = directory.replace("/", ".").replace("\\", ".")
for filename in os.listdir(directory):
if not filename.endswith(".py") or filename == "__init__.py":
continue
__import__(f"{base}.{filename[:-3]}")
return self
def with_routes(self, routes: list[Route]) -> "WebApp":
self._check_for_app()
assert self._routes is not None, "routes must not be None"
assert all(isinstance(route, Route) for route in routes), "all routes must be of type starlette.routing.Route"
self._routes.extend(routes)
return self
def with_route(self, path: str, fn: Callable[[Request], Any], method: HTTPMethods, **kwargs) -> "WebApp":
self._check_for_app()
assert path is not None, "path must not be None"
assert fn is not None, "fn must not be None"
assert method in ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"], "method must be a valid HTTP method"
self._routes.append(Route(path, fn, methods=[method], **kwargs))
return self
def with_middleware(self, middleware: PartialMiddleware) -> "WebApp":
self._check_for_app()
if isinstance(middleware, Middleware):
self._middleware.append(middleware)
elif callable(middleware):
self._middleware.append(Middleware(middleware))
else:
raise ValueError("middleware must be of type starlette.middleware.Middleware or a callable")
return self
def main(self):
_logger.debug(f"Preparing API")
if self._app is None:
routes = [
Route(
path=route.path,
endpoint=self._services.inject(route.endpoint),
methods=route.methods,
name=route.name,
)
for route in self._routes + Router.get_routes()
]
app = Starlette(
routes=routes,
middleware=[
*self._middleware,
Middleware(
CORSMiddleware,
allow_origins=self._get_allowed_origins(),
allow_methods=["*"],
allow_headers=["*"],
),
],
exception_handlers=self._exception_handlers,
)
else:
app = self._app
_logger.info(f"Start API on {self._api_settings.host}:{self._api_settings.port}")
uvicorn.run(
app,
host=self._api_settings.host,
port=self._api_settings.port,
log_config=None,
)
_logger.info("Shutdown API")

View File

@@ -0,0 +1,30 @@
[build-system]
requires = ["setuptools>=70.1.0", "wheel>=0.43.0"]
build-backend = "setuptools.build_meta"
[project]
name = "cpl-api"
version = "2024.7.0"
description = "CPL api"
readme ="CPL api package"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [
{ name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" }
]
keywords = ["cpl", "api", "backend", "shared", "library"]
dynamic = ["dependencies", "optional-dependencies"]
[project.urls]
Homepage = "https://www.sh-edraft.de"
[tool.setuptools.packages.find]
where = ["."]
include = ["cpl*"]
[tool.setuptools.dynamic]
dependencies = { file = ["requirements.txt"] }
optional-dependencies.dev = { file = ["requirements.dev.txt"] }

View File

@@ -0,0 +1 @@
black==25.1.0

View File

@@ -0,0 +1,6 @@
cpl-auth
cpl-application
cpl-core
cpl-dependency
starlette==0.48.0
python-multipart==0.0.20

View File

@@ -1,10 +1,18 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Callable, Self
from cpl.application.host import Host from cpl.application.host import Host
from cpl.core.console.console import Console from cpl.core.console.console import Console
from cpl.core.log import LogSettings
from cpl.core.log.log_level import LogLevel
from cpl.core.log.logger_abc import LoggerABC
from cpl.dependency.service_provider_abc import ServiceProviderABC from cpl.dependency.service_provider_abc import ServiceProviderABC
def __not_implemented__(package: str, func: Callable):
raise NotImplementedError(f"Package {package} is required to use {func.__name__} method")
class ApplicationABC(ABC): class ApplicationABC(ABC):
r"""ABC for the Application class r"""ABC for the Application class
@@ -17,6 +25,53 @@ class ApplicationABC(ABC):
def __init__(self, services: ServiceProviderABC): def __init__(self, services: ServiceProviderABC):
self._services = services self._services = services
@classmethod
def extend(cls, name: str | Callable, func: Callable[[Self], Self]):
r"""Extend the Application with a custom method
Parameters:
name: :class:`str`
Name of the method
func: :class:`Callable[[Self], Self]`
Function that takes the Application as a parameter and returns it
"""
if callable(name):
name = name.__name__
setattr(cls, name, func)
return cls
def with_logging(self, level: LogLevel = None):
if level is None:
from cpl.core.configuration.configuration import Configuration
settings = Configuration.get(LogSettings)
level = settings.level if settings else LogLevel.info
logger = self._services.get_service(LoggerABC)
logger.set_level(level)
def with_permissions(self, *args, **kwargs):
__not_implemented__("cpl-auth", self.with_permissions)
def with_migrations(self, *args, **kwargs):
__not_implemented__("cpl-database", self.with_migrations)
def with_seeders(self, *args, **kwargs):
__not_implemented__("cpl-database", self.with_seeders)
def with_extension(self, func: Callable[[Self, ...], None], *args, **kwargs):
r"""Extend the Application with a custom method
Parameters:
func: :class:`Callable[[Self], Self]`
Function that takes the Application as a parameter and returns it
"""
assert func is not None, "func must not be None"
assert callable(func), "func must be callable"
func(self, *args, **kwargs)
def run(self): def run(self):
r"""Entry point r"""Entry point
@@ -28,6 +83,4 @@ class ApplicationABC(ABC):
Console.close() Console.close()
@abstractmethod @abstractmethod
def main(self): def main(self): ...
r"""Main method of application"""
pass

View File

@@ -4,10 +4,7 @@ from cpl.dependency import ServiceProviderABC
class ApplicationExtensionABC(ABC): class ApplicationExtensionABC(ABC):
@abstractmethod
def __init__(self):
pass
@staticmethod
@abstractmethod @abstractmethod
def run(self, services: ServiceProviderABC): def run(services: ServiceProviderABC): ...
pass

View File

@@ -6,17 +6,14 @@ from cpl.dependency.service_collection import ServiceCollection
class StartupABC(ABC): class StartupABC(ABC):
r"""ABC for the startup class""" r"""ABC for the startup class"""
@staticmethod
@abstractmethod @abstractmethod
def __init__(self): def configure_configuration():
pass r"""Creates configuration of application"""
@staticmethod
@abstractmethod @abstractmethod
def configure_configuration(self): def configure_services(service: ServiceCollection):
r"""Creates configuration of application
"""
@abstractmethod
def configure_services(self, service: ServiceCollection):
r"""Creates service provider r"""Creates service provider
Parameter: Parameter:

View File

@@ -6,18 +6,14 @@ from cpl.dependency import ServiceCollection
class StartupExtensionABC(ABC): class StartupExtensionABC(ABC):
r"""ABC for startup extension classes""" r"""ABC for startup extension classes"""
@staticmethod
@abstractmethod @abstractmethod
def __init__(self): def configure_configuration():
pass r"""Creates configuration of application"""
@staticmethod
@abstractmethod @abstractmethod
def configure_configuration(self): def configure_services(services: ServiceCollection):
r"""Creates configuration of application
"""
@abstractmethod
def configure_services(self, services: ServiceCollection):
r"""Creates service provider r"""Creates service provider
Parameter: Parameter:
services: :class:`cpl.dependency.service_collection` services: :class:`cpl.dependency.service_collection`

View File

@@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import Type, Optional, Callable from typing import Type, Optional, TypeVar, Generic
from cpl.application.abc.application_abc import ApplicationABC from cpl.application.abc.application_abc import ApplicationABC
from cpl.application.abc.application_extension_abc import ApplicationExtensionABC from cpl.application.abc.application_extension_abc import ApplicationExtensionABC
@@ -8,9 +8,10 @@ from cpl.application.abc.startup_extension_abc import StartupExtensionABC
from cpl.application.host import Host from cpl.application.host import Host
from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.service_collection import ServiceCollection
TApp = TypeVar("TApp", bound=ApplicationABC)
class ApplicationBuilder:
r"""A builder for constructing an application with configurable services and extensions.""" class ApplicationBuilder(Generic[TApp]):
def __init__(self, app: Type[ApplicationABC]): def __init__(self, app: Type[ApplicationABC]):
assert app is not None, "app must not be None" assert app is not None, "app must not be None"
@@ -34,11 +35,11 @@ class ApplicationBuilder:
def service_provider(self): def service_provider(self):
return self._services.build() return self._services.build()
def use_startup(self, startup: Type[StartupABC]) -> "ApplicationBuilder": def with_startup(self, startup: Type[StartupABC]) -> "ApplicationBuilder":
self._startup = startup() self._startup = startup
return self return self
def use_extension( def with_extension(
self, self,
extension: Type[ApplicationExtensionABC | StartupExtensionABC], extension: Type[ApplicationExtensionABC | StartupExtensionABC],
) -> "ApplicationBuilder": ) -> "ApplicationBuilder":
@@ -49,9 +50,8 @@ class ApplicationBuilder:
return self return self
def build(self) -> ApplicationABC: def build(self) -> TApp:
for ex in self._startup_extensions: for extension in self._startup_extensions:
extension = ex()
Host.run(extension.configure_configuration) Host.run(extension.configure_configuration)
Host.run(extension.configure_services, self._services) Host.run(extension.configure_services, self._services)
@@ -59,8 +59,7 @@ class ApplicationBuilder:
Host.run(self._startup.configure_configuration) Host.run(self._startup.configure_configuration)
Host.run(self._startup.configure_services, self._services) Host.run(self._startup.configure_services, self._services)
for ex in self._app_extensions: for extension in self._app_extensions:
extension = ex()
Host.run(extension.run, self.service_provider) Host.run(extension.run, self.service_provider)
return self._app(self.service_provider) return self._app(self.service_provider)

View File

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

View File

@@ -1,13 +1,24 @@
from cpl.auth import permission as _permission from enum import Enum
from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin from typing import Type
from cpl.auth.keycloak.keycloak_client import KeycloakClient
from cpl.dependency import ServiceCollection as _ServiceCollection
from cpl.application.abc import ApplicationABC as _ApplicationABC
from cpl.auth import permission as _permission
from cpl.auth.keycloak.keycloak_admin import KeycloakAdmin as _KeycloakAdmin
from cpl.auth.keycloak.keycloak_client import KeycloakClient as _KeycloakClient
from cpl.dependency.service_collection import ServiceCollection as _ServiceCollection
from .auth_logger import AuthLogger from .auth_logger import AuthLogger
from .keycloak_settings import KeycloakSettings from .keycloak_settings import KeycloakSettings
from .permission_seeder import PermissionSeeder from .permission_seeder import PermissionSeeder
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
def _add_daos(collection: _ServiceCollection): def _add_daos(collection: _ServiceCollection):
from .schema._administration.auth_user_dao import AuthUserDao from .schema._administration.auth_user_dao import AuthUserDao
from .schema._administration.api_key_dao import ApiKeyDao from .schema._administration.api_key_dao import ApiKeyDao
@@ -34,8 +45,8 @@ def add_auth(collection: _ServiceCollection):
from cpl.database.model.server_type import ServerType, ServerTypes from cpl.database.model.server_type import ServerType, ServerTypes
try: try:
collection.add_singleton(KeycloakClient) collection.add_singleton(_KeycloakClient)
collection.add_singleton(KeycloakAdmin) collection.add_singleton(_KeycloakAdmin)
_add_daos(collection) _add_daos(collection)
@@ -68,3 +79,4 @@ def add_permission(collection: _ServiceCollection):
_ServiceCollection.with_module(add_auth, __name__) _ServiceCollection.with_module(add_auth, __name__)
_ServiceCollection.with_module(add_permission, _permission.__name__) _ServiceCollection.with_module(add_permission, _permission.__name__)
_ApplicationABC.extend(_ApplicationABC.with_permissions, _with_permissions)

View File

@@ -1,37 +1,17 @@
from typing import Optional from typing import Optional
from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl.core.environment import Environment
class KeycloakSettings(ConfigurationModelABC): class KeycloakSettings(ConfigurationModelABC):
def __init__( def __init__(
self, self,
url: str = Environment.get("KEYCLOAK_URL", str), src: Optional[dict] = None,
client_id: str = Environment.get("KEYCLOAK_CLIENT_ID", str),
realm: str = Environment.get("KEYCLOAK_REALM", str),
client_secret: str = Environment.get("KEYCLOAK_CLIENT_SECRET", str),
): ):
ConfigurationModelABC.__init__(self) ConfigurationModelABC.__init__(self, src, "KEYCLOAK")
self._url: Optional[str] = url self.option("url", str, required=True)
self._client_id: Optional[str] = client_id self.option("client_id", str, required=True)
self._realm: Optional[str] = realm self.option("realm", str, required=True)
self._client_secret: Optional[str] = client_secret self.option("client_secret", str, required=True)
@property
def url(self) -> Optional[str]:
return self._url
@property
def client_id(self) -> Optional[str]:
return self._client_id
@property
def realm(self) -> Optional[str]:
return self._realm
@property
def client_secret(self) -> Optional[str]:
return self._client_secret

View File

@@ -1,25 +1,27 @@
import secrets import secrets
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Union
from async_property import async_property from async_property import async_property
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.core.environment import Environment from cpl.core.environment.environment import Environment
from cpl.core.log import Logger from cpl.core.log.logger import Logger
from cpl.core.typing import SerialId, Id from cpl.core.typing import Id, SerialId
from cpl.database.abc import DbModelABC from cpl.core.utils.credential_manager import CredentialManager
from cpl.dependency import ServiceProviderABC from cpl.database.abc.db_model_abc import DbModelABC
from cpl.dependency.service_provider_abc import ServiceProviderABC
_logger = Logger(__name__) _logger = Logger(__name__)
class ApiKey(DbModelABC): class ApiKey(DbModelABC):
def __init__( def __init__(
self, self,
id: SerialId, id: SerialId,
identifier: str, identifier: str,
key: str, key: Union[str, bytes],
deleted: bool = False, deleted: bool = False,
editor_id: Optional[Id] = None, editor_id: Optional[Id] = None,
created: Optional[datetime] = None, created: Optional[datetime] = None,
@@ -37,12 +39,17 @@ class ApiKey(DbModelABC):
def key(self) -> str: def key(self) -> str:
return self._key return self._key
@property
def plain_key(self) -> str:
return CredentialManager.decrypt(self.key)
@async_property @async_property
async def permissions(self): async def permissions(self):
from cpl.auth.schema._permission.api_key_permission_dao import ApiKeyPermissionDao from cpl.auth.schema._permission.api_key_permission_dao import ApiKeyPermissionDao
api_key_permission_dao: ApiKeyPermissionDao = ServiceProviderABC.get_global_service(ApiKeyPermissionDao) apiKeyPermissionDao = ServiceProviderABC.get_global_provider().get_service(ApiKeyPermissionDao)
return [await x.permission for x in await api_key_permission_dao.find_by_api_key_id(self.id)]
return [await x.permission for x in await apiKeyPermissionDao.find_by_api_key_id(self.id)]
async def has_permission(self, permission: Permissions) -> bool: async def has_permission(self, permission: Permissions) -> bool:
return permission.value in [x.name for x in await self.permissions] return permission.value in [x.name for x in await self.permissions]
@@ -52,7 +59,7 @@ class ApiKey(DbModelABC):
@staticmethod @staticmethod
def new_key() -> str: def new_key() -> str:
return f"api_{secrets.token_urlsafe(Environment.get("API_KEY_LENGTH", int, 64))}" return CredentialManager.encrypt(f"api_{secrets.token_urlsafe(Environment.get("API_KEY_LENGTH", int, 64))}")
@classmethod @classmethod
def new(cls, identifier: str) -> "ApiKey": def new(cls, identifier: str) -> "ApiKey":

View File

@@ -5,7 +5,7 @@ from typing import Optional
from async_property import async_property from async_property import async_property
from keycloak import KeycloakGetError from keycloak import KeycloakGetError
from cpl.auth import KeycloakAdmin from cpl.auth.keycloak import KeycloakAdmin
from cpl.auth.auth_logger import AuthLogger from cpl.auth.auth_logger import AuthLogger
from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions import Permissions
from cpl.core.typing import SerialId from cpl.core.typing import SerialId

View File

@@ -2,14 +2,13 @@ import inspect
import json import json
import os import os
import sys import sys
from inspect import isclass
from typing import Any from typing import Any
from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl.core.console.console import Console from cpl.core.console.console import Console
from cpl.core.console.foreground_color_enum import ForegroundColorEnum from cpl.core.console.foreground_color_enum import ForegroundColorEnum
from cpl.core.environment.environment import Environment
from cpl.core.typing import D, T from cpl.core.typing import D, T
from cpl.core.utils.json_processor import JSONProcessor
class Configuration: class Configuration:
@@ -88,6 +87,8 @@ class Configuration:
if os.path.isabs(name): if os.path.isabs(name):
file_path = name file_path = name
else: else:
from cpl.core.environment import Environment
path_root = Environment.get_cwd() path_root = Environment.get_cwd()
if path is not None: if path is not None:
path_root = path path_root = path
@@ -115,9 +116,7 @@ class Configuration:
if sub.__name__ != key and sub.__name__.replace("Settings", "") != key: if sub.__name__ != key and sub.__name__.replace("Settings", "") != key:
continue continue
configuration = JSONProcessor.process(sub, value) cls.set(sub, sub(value))
cls.set(sub, configuration)
@classmethod @classmethod
def set(cls, key: Any, value: T): def set(cls, key: Any, value: T):
@@ -128,7 +127,11 @@ class Configuration:
@classmethod @classmethod
def get(cls, key: Any, default: D = None) -> T | D: def get(cls, key: Any, default: D = None) -> T | D:
if inspect.isclass(key): key_name = key.__name__ if inspect.isclass(key) else key
key = key.__name__
return cls._config.get(key, default) result = cls._config.get(key_name, default)
if issubclass(key, ConfigurationModelABC) and result == default:
result = key()
cls.set(key, result)
return result

View File

@@ -1,5 +1,82 @@
from abc import ABC from abc import ABC, abstractmethod
from typing import Optional, Type, Any
from cpl.core.typing import T
from cpl.core.utils.cast import cast
from cpl.core.utils.get_value import get_value
from cpl.core.utils.string import String
class ConfigurationModelABC(ABC): class ConfigurationModelABC(ABC):
pass r"""
ABC for configuration model classes
"""
@abstractmethod
def __init__(
self,
src: Optional[dict] = None,
env_prefix: Optional[str] = None,
readonly: bool = True,
):
ABC.__init__(self)
self._src = src or {}
self._options: dict[str, Any] = {}
self._env_prefix = env_prefix
self._readonly = readonly
def __setattr__(self, attr: str, value: Any):
if hasattr(self, "_readonly") and self._readonly:
raise AttributeError(f"Cannot set attribute: {attr}. {type(self).__name__} is read-only")
super().__setattr__(attr, value)
def __getattr__(self, attr: str) -> Any:
options = super().__getattribute__("_options")
if attr in options:
return options[attr]
return super().__getattribute__(attr)
def option(self, field: str, cast_type: Type[T], default=None, required=False, from_env=True):
value = None
field_variants = [
field,
String.first_to_upper(field),
String.first_to_lower(field),
String.to_camel_case(field),
String.to_snake_case(field),
String.to_pascal_case(field),
]
value = None
for variant in field_variants:
if variant in self._src:
value = self._src[variant]
break
if value is None and from_env:
from cpl.core.environment import Environment
env_field = field.upper()
if self._env_prefix:
env_field = f"{self._env_prefix}_{env_field}"
value = cast(Environment.get(env_field, str), cast_type)
if value is None and required:
raise ValueError(f"{field} is required")
elif value is None:
self._options[field] = default
return
self._options[field] = cast(value, cast_type)
def get(self, field: str, default=None) -> Optional[T]:
return get_value(self._src, field, self._options[field].type, default)
def to_dict(self) -> dict:
return {field: self.get(field) for field in self._options.keys()}

View File

@@ -1,9 +1,9 @@
import os import os
from socket import gethostname from socket import gethostname
from typing import Optional, Type from typing import Type
from cpl.core.environment.environment_enum import EnvironmentEnum from cpl.core.environment.environment_enum import EnvironmentEnum
from cpl.core.typing import T from cpl.core.typing import T, D
from cpl.core.utils.get_value import get_value from cpl.core.utils.get_value import get_value
@@ -55,14 +55,14 @@ class Environment:
os.environ[key] = str(value) os.environ[key] = str(value)
@staticmethod @staticmethod
def get(key: str, cast_type: Type[T], default: Optional[T] = None) -> Optional[T]: def get(key: str, cast_type: Type[T], default: D = None) -> T | D:
""" """
Get an environment variable and cast it to a specified type. Get an environment variable and cast it to a specified type.
:param str key: The name of the environment variable. :param str key: The name of the environment variable.
:param Type[T] cast_type: A callable to cast the variable's value. :param Type[T] cast_type: A callable to cast the variable's value.
:param Optional[T] default: The default value to return if the variable is not found. Defaults to None.The default value to return if the variable is not found. Defaults to None. :param T default: The default value to return if the variable is not found. Defaults to None.The default value to return if the variable is not found. Defaults to None.
:return: The casted value, or None if the variable is not found. :return: The casted value, or None if the variable is not found.
:rtype: Optional[T] :rtype: T | D
""" """
return get_value(dict(os.environ), key, cast_type, default) return get_value(dict(os.environ), key, cast_type, default)

View File

@@ -1,4 +1,4 @@
from .logger import Logger from .logger import Logger
from .logger_abc import LoggerABC from .logger_abc import LoggerABC
from .log_level_enum import LogLevelEnum from .log_level import LogLevel
from .logging_settings import LogSettings from .log_settings import LogSettings

View File

@@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
class LogLevelEnum(Enum): class LogLevel(Enum):
off = "OFF" # Nothing off = "OFF" # Nothing
trace = "TRC" # Detailed app information's trace = "TRC" # Detailed app information's
debug = "DEB" # Detailed app state debug = "DEB" # Detailed app state

View File

@@ -0,0 +1,18 @@
from typing import Optional
from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl.core.log.log_level import LogLevel
class LogSettings(ConfigurationModelABC):
def __init__(
self,
src: Optional[dict] = None,
):
ConfigurationModelABC.__init__(self, src, "LOG")
self.option("path", str, default="logs")
self.option("filename", str, default="app.log")
self.option("console", LogLevel, default=LogLevel.info)
self.option("level", LogLevel, default=LogLevel.info)

View File

@@ -3,28 +3,30 @@ import traceback
from datetime import datetime from datetime import datetime
from cpl.core.console import Console from cpl.core.console import Console
from cpl.core.log.log_level_enum import LogLevelEnum from cpl.core.log.log_level import LogLevel
from cpl.core.log.logger_abc import LoggerABC from cpl.core.log.logger_abc import LoggerABC
from cpl.core.typing import Messages, Source from cpl.core.typing import Messages, Source
class Logger(LoggerABC): class Logger(LoggerABC):
_level = LogLevelEnum.info _levels = [x for x in LogLevel]
_levels = [x for x in LogLevelEnum]
# ANSI color codes for different log levels # ANSI color codes for different log levels
_COLORS = { _COLORS = {
LogLevelEnum.trace: "\033[37m", # Light Gray LogLevel.trace: "\033[37m", # Light Gray
LogLevelEnum.debug: "\033[94m", # Blue LogLevel.debug: "\033[94m", # Blue
LogLevelEnum.info: "\033[92m", # Green LogLevel.info: "\033[92m", # Green
LogLevelEnum.warning: "\033[93m", # Yellow LogLevel.warning: "\033[93m", # Yellow
LogLevelEnum.error: "\033[91m", # Red LogLevel.error: "\033[91m", # Red
LogLevelEnum.fatal: "\033[95m", # Magenta LogLevel.fatal: "\033[95m", # Magenta
} }
def __init__(self, source: Source, file_prefix: str = None): def __init__(self, source: Source, file_prefix: str = None):
LoggerABC.__init__(self) LoggerABC.__init__(self)
assert source is not None and source != "", "Source cannot be None or empty"
if source == LoggerABC.__name__:
source = None
self._source = source self._source = source
if file_prefix is None: if file_prefix is None:
@@ -33,6 +35,13 @@ class Logger(LoggerABC):
self._file_prefix = file_prefix self._file_prefix = file_prefix
self._create_log_dir() self._create_log_dir()
@property
def _settings(self):
from cpl.core.configuration.configuration import Configuration
from cpl.core.log.log_settings import LogSettings
return Configuration.get(LogSettings)
@property @property
def log_file(self): def log_file(self):
return f"logs/{self._file_prefix}_{datetime.now().strftime('%Y-%m-%d')}.log" return f"logs/{self._file_prefix}_{datetime.now().strftime('%Y-%m-%d')}.log"
@@ -45,7 +54,7 @@ class Logger(LoggerABC):
os.makedirs("logs") os.makedirs("logs")
@classmethod @classmethod
def set_level(cls, level: LogLevelEnum): def set_level(cls, level: LogLevel):
if level in cls._levels: if level in cls._levels:
cls._level = level cls._level = level
else: else:
@@ -62,23 +71,32 @@ class Logger(LoggerABC):
f"{log_file.split('.log')[0]}_{datetime.now().strftime('%H-%M-%S')}.log", f"{log_file.split('.log')[0]}_{datetime.now().strftime('%H-%M-%S')}.log",
) )
def _write_log_to_file(self, content: str): def _should_log(self, input_level: LogLevel, settings_level: LogLevel) -> bool:
return self._levels.index(input_level) >= self._levels.index(settings_level)
def _write_log_to_file(self, level: LogLevel, content: str):
if not self._should_log(level, self._settings.level):
return
file = self.log_file file = self.log_file
self._ensure_file_size(file) self._ensure_file_size(file)
with open(file, "a") as log_file: with open(file, "a") as log_file:
log_file.write(content + "\n") log_file.write(content + "\n")
log_file.close() log_file.close()
def _log(self, level: LogLevelEnum, *messages: Messages): def _write_to_console(self, level: LogLevel, content: str):
try: if not self._should_log(level, self._settings.console):
if self._levels.index(level) < self._levels.index(self._level): return
return
Console.write_line(f"{self._COLORS.get(level, '\033[0m')}{content}\033[0m")
def _log(self, level: LogLevel, *messages: Messages):
try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
formatted_message = self._format_message(level.value, timestamp, *messages) formatted_message = self._format_message(level.value, timestamp, *messages)
self._write_log_to_file(formatted_message) self._write_log_to_file(level, formatted_message)
Console.write_line(f"{self._COLORS.get(self._level, '\033[0m')}{formatted_message}\033[0m") self._write_to_console(level, formatted_message)
except Exception as e: except Exception as e:
print(f"Error while logging: {e} -> {traceback.format_exc()}") print(f"Error while logging: {e} -> {traceback.format_exc()}")
@@ -91,27 +109,35 @@ class Logger(LoggerABC):
messages = [str(message) for message in messages if message is not None] messages = [str(message) for message in messages if message is not None]
return f"<{timestamp}> [{level.upper():^3}] [{self._file_prefix}] - [{self._source}]: {' '.join(messages)}" message = f"<{timestamp}>"
message += f" [{level.upper():^3}]"
message += f" [{self._file_prefix}]"
if self._source is not None:
message += f" - [{self._source}]"
message += f": {' '.join(messages)}"
return message
def header(self, string: str): def header(self, string: str):
self._log(LogLevelEnum.info, string) self._log(LogLevel.info, string)
def trace(self, *messages: Messages): def trace(self, *messages: Messages):
self._log(LogLevelEnum.trace, *messages) self._log(LogLevel.trace, *messages)
def debug(self, *messages: Messages): def debug(self, *messages: Messages):
self._log(LogLevelEnum.debug, *messages) self._log(LogLevel.debug, *messages)
def info(self, *messages: Messages): def info(self, *messages: Messages):
self._log(LogLevelEnum.info, *messages) self._log(LogLevel.info, *messages)
def warning(self, *messages: Messages): def warning(self, *messages: Messages):
self._log(LogLevelEnum.warning, *messages) self._log(LogLevel.warning, *messages)
def error(self, message, e: Exception = None): def error(self, message, e: Exception = None):
self._log(LogLevelEnum.error, message, f"{e} -> {traceback.format_exc()}" if e else None) self._log(LogLevel.error, message, f"{e} -> {traceback.format_exc()}" if e else None)
def fatal(self, message, e: Exception = None, prevent_quit: bool = False): def fatal(self, message, e: Exception = None, prevent_quit: bool = False):
self._log(LogLevelEnum.fatal, message, f"{e} -> {traceback.format_exc()}" if e else None) self._log(LogLevel.fatal, message, f"{e} -> {traceback.format_exc()}" if e else None)
if not prevent_quit: if not prevent_quit:
exit(-1) exit(-1)

View File

@@ -1,5 +1,6 @@
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from cpl.core.log.log_level import LogLevel
from cpl.core.typing import Messages from cpl.core.typing import Messages
@@ -7,12 +8,10 @@ class LoggerABC(ABC):
r"""ABC for :class:`cpl.core.log.logger_service.Logger`""" r"""ABC for :class:`cpl.core.log.logger_service.Logger`"""
@abstractmethod @abstractmethod
def set_level(self, level: str): def set_level(self, level: LogLevel): ...
pass
@abstractmethod @abstractmethod
def _format_message(self, level: str, timestamp, *messages: Messages) -> str: def _format_message(self, level: str, timestamp, *messages: Messages) -> str: ...
pass
@abstractmethod @abstractmethod
def header(self, string: str): def header(self, string: str):

View File

@@ -1,53 +0,0 @@
from typing import Optional
from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl.core.log.log_level_enum import LogLevelEnum
class LogSettings(ConfigurationModelABC):
r"""Representation of logging settings"""
def __init__(
self,
path: str = None,
filename: str = None,
console_log_level: LogLevelEnum = None,
file_log_level: LogLevelEnum = None,
):
ConfigurationModelABC.__init__(self)
self._path: Optional[str] = path
self._filename: Optional[str] = filename
self._console: Optional[LogLevelEnum] = console_log_level
self._level: Optional[LogLevelEnum] = file_log_level
@property
def path(self) -> str:
return self._path
@path.setter
def path(self, path: str) -> None:
self._path = path
@property
def filename(self) -> str:
return self._filename
@filename.setter
def filename(self, filename: str) -> None:
self._filename = filename
@property
def console(self) -> LogLevelEnum:
return self._console
@console.setter
def console(self, console: LogLevelEnum) -> None:
self._console = console
@property
def level(self) -> LogLevelEnum:
return self._level
@level.setter
def level(self, level: LogLevelEnum) -> None:
self._level = level

View File

@@ -7,10 +7,8 @@ from cpl.core.typing import T
class PipeABC(ABC, Generic[T]): class PipeABC(ABC, Generic[T]):
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def to_str(value: T, *args) -> str: def to_str(value: T, *args) -> str: ...
pass
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def from_str(value: str, *args) -> T: def from_str(value: str, *args) -> T: ...
pass

View File

@@ -3,3 +3,4 @@ from .credential_manager import CredentialManager
from .json_processor import JSONProcessor from .json_processor import JSONProcessor
from .pip import Pip from .pip import Pip
from .string import String from .string import String
from .get_value import get_value

View File

@@ -0,0 +1,69 @@
from enum import Enum
from typing import Type, Any
from cpl.core.typing import T
def _cast_enum(value: str, enum_type: Type[Enum]) -> Enum:
try:
return enum_type(value)
except ValueError:
pass
try:
return enum_type(value.lower())
except ValueError:
pass
try:
return enum_type(value.upper())
except ValueError:
pass
try:
return enum_type[value]
except KeyError:
pass
try:
return enum_type[value.lower()]
except KeyError:
pass
try:
return enum_type[value.upper()]
except KeyError:
pass
raise ValueError(f"Cannot cast value '{value}' to enum '{enum_type.__name__}'")
def cast(value: Any, cast_type: Type[T], list_delimiter: str = ",") -> T:
"""
Cast a value to a specified type.
:param Any value: Value to be casted.
:param Type[T] cast_type: A callable to cast the variable's value.
:param str list_delimiter: The delimiter to split the value into a list. Defaults to ",".
:return:
"""
if value is None:
return None
if cast_type == bool:
return value.lower() in ["true", "1", "yes", "on"]
if (cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__) == list:
if not (value.startswith("[") and value.endswith("]")) and list_delimiter not in value:
raise ValueError("List values must be enclosed in square brackets or use a delimiter.")
if value.startswith("[") and value.endswith("]"):
value = value[1:-1]
value = value.split(list_delimiter)
subtype = cast_type.__args__[0] if hasattr(cast_type, "__args__") else None
return [subtype(item) if subtype is not None else item for item in value]
if isinstance(cast_type, type) and issubclass(cast_type, Enum):
return _cast_enum(value, cast_type)
return cast_type(value)

View File

@@ -1,12 +1,42 @@
import base64 import os
from cryptography.fernet import Fernet
from cpl.core.log.logger import Logger
_logger = Logger(__name__)
class CredentialManager: class CredentialManager:
r"""Handles credential encryption and decryption""" r"""Handles credential encryption and decryption"""
@staticmethod _secret: str = None
def encrypt(string: str) -> str:
r"""Encode with base64 @classmethod
def with_secret(cls, file: str = None):
if file is None:
file = ".secret"
if not os.path.isfile(file):
dirname = os.path.dirname(file)
if dirname != "":
os.makedirs(dirname, exist_ok=True)
with open(file, "w") as secret_file:
secret_file.write(Fernet.generate_key().decode())
secret_file.close()
_logger.warning("Secret file not found, regenerating")
with open(file, "r") as secret_file:
secret = secret_file.read().strip()
if secret == "" or secret is None:
_logger.fatal("No secret found in .secret file.")
cls._secret = str(secret)
@classmethod
def encrypt(cls, string: str) -> str:
r"""Encode with Fernet
Parameter: Parameter:
string: :class:`str` string: :class:`str`
@@ -15,11 +45,11 @@ class CredentialManager:
Returns: Returns:
Encoded string Encoded string
""" """
return base64.b64encode(string.encode("utf-8")).decode("utf-8") return Fernet(cls._secret).encrypt(string.encode()).decode()
@staticmethod @classmethod
def decrypt(string: str) -> str: def decrypt(cls, string: str) -> str:
r"""Decode with base64 r"""Decode with Fernet
Parameter: Parameter:
string: :class:`str` string: :class:`str`
@@ -28,19 +58,4 @@ class CredentialManager:
Returns: Returns:
Decoded string Decoded string
""" """
return base64.b64decode(string).decode("utf-8") return Fernet(cls._secret).decrypt(string).decode()
@staticmethod
def build_string(string: str, credentials: str):
r"""Builds string with credentials in it
Parameter:
string: :class:`str`
String in which the variable is replaced by credentials
credentials: :class:`str`
String to encode
Returns:
Decoded string
"""
return string.replace("$credentials", CredentialManager.decrypt(credentials))

View File

@@ -1,6 +1,7 @@
from typing import Type, Optional from typing import Type, Optional
from cpl.core.typing import T from cpl.core.typing import T
from cpl.core.utils.cast import cast
def get_value( def get_value(
@@ -37,20 +38,9 @@ def get_value(
return value return value
try: try:
if cast_type == bool: cast(value, cast_type, list_delimiter)
return value.lower() in ["true", "1"]
if (cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__) == list:
if not (value.startswith("[") and value.endswith("]")) and list_delimiter not in value:
raise ValueError("List values must be enclosed in square brackets or use a delimiter.")
if value.startswith("[") and value.endswith("]"):
value = value[1:-1]
value = value.split(list_delimiter)
subtype = cast_type.__args__[0] if hasattr(cast_type, "__args__") else None
return [subtype(item) if subtype is not None else item for item in value]
return cast_type(value)
except (ValueError, TypeError): except (ValueError, TypeError):
from cpl.core.log import Logger
Logger(__name__).debug(f"Failed to cast value '{value}' to type '{cast_type.__name__}'")
return default return default

View File

@@ -17,7 +17,36 @@ class String:
Returns: Returns:
String converted to CamelCase String converted to CamelCase
""" """
return re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
parts = re.split(r"[^a-zA-Z0-9]+", s.strip())
parts = [p for p in parts if p]
if not parts:
return ""
return parts[0].lower() + "".join(word.capitalize() for word in parts[1:])
@staticmethod
def to_pascal_case(s: str) -> str:
r"""Converts string to pascal case
Parameter:
chars: :class:`str`
String to convert
Returns:
String converted to PascalCase
"""
parts = re.split(r"[^a-zA-Z0-9]+", s.strip())
parts = [p for p in parts if p]
if not parts:
return ""
return "".join(word.capitalize() for word in parts)
@staticmethod @staticmethod
def to_snake_case(chars: str) -> str: def to_snake_case(chars: str) -> str:

View File

@@ -1,11 +1,33 @@
from typing import Type from typing import Type
from cpl.application.abc import ApplicationABC as _ApplicationABC
from cpl.dependency import ServiceCollection as _ServiceCollection from cpl.dependency import ServiceCollection as _ServiceCollection
from . import mysql as _mysql from . import mysql as _mysql
from . import postgres as _postgres from . import postgres as _postgres
from .table_manager import TableManager from .table_manager import TableManager
def _with_migrations(self: _ApplicationABC, *paths: list[str]) -> _ApplicationABC:
from cpl.application.host import Host
from cpl.database.service.migration_service import MigrationService
migration_service = self._services.get_service(MigrationService)
migration_service.with_directory("./scripts")
Host.run(migration_service.migrate)
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
def _add(collection: _ServiceCollection, db_context: Type, default_port: int, server_type: str): def _add(collection: _ServiceCollection, db_context: Type, default_port: int, server_type: str):
from cpl.core.console import Console from cpl.core.console import Console
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
@@ -42,6 +64,7 @@ def add_postgres(collection: _ServiceCollection):
_add(collection, DBContext, 5432, ServerTypes.POSTGRES.value) _add(collection, DBContext, 5432, ServerTypes.POSTGRES.value)
_ServiceCollection.with_module(add_mysql, _mysql.__name__) _ServiceCollection.with_module(add_mysql, _mysql.__name__)
_ServiceCollection.with_module(add_postgres, _postgres.__name__) _ServiceCollection.with_module(add_postgres, _postgres.__name__)
_ApplicationABC.extend(_ApplicationABC.with_migrations, _with_migrations)
_ApplicationABC.extend(_ApplicationABC.with_seeders, _with_seeders)

View File

@@ -9,18 +9,15 @@ class ConnectionABC(ABC):
r"""ABC for the :class:`cpl.database.connection.database_connection.DatabaseConnection`""" r"""ABC for the :class:`cpl.database.connection.database_connection.DatabaseConnection`"""
@abstractmethod @abstractmethod
def __init__(self): def __init__(self): ...
pass
@property @property
@abstractmethod @abstractmethod
def server(self) -> MySQLConnectionAbstract: def server(self) -> MySQLConnectionAbstract: ...
pass
@property @property
@abstractmethod @abstractmethod
def cursor(self) -> MySQLCursorBuffered: def cursor(self) -> MySQLCursorBuffered: ...
pass
@abstractmethod @abstractmethod
def connect(self, database_settings: DatabaseSettings): def connect(self, database_settings: DatabaseSettings):

View File

@@ -4,10 +4,9 @@ from enum import Enum
from types import NoneType from types import NoneType
from typing import Generic, Optional, Union, Type, List, Any from typing import Generic, Optional, Union, Type, List, Any
from cpl.core.ctx import get_user
from cpl.core.typing import T, Id from cpl.core.typing import T, Id
from cpl.core.utils import String
from cpl.core.utils.get_value import get_value from cpl.core.utils.get_value import get_value
from cpl.core.utils.string import String
from cpl.database.abc.db_context_abc import DBContextABC from cpl.database.abc.db_context_abc import DBContextABC
from cpl.database.const import DATETIME_FORMAT from cpl.database.const import DATETIME_FORMAT
from cpl.database.db_logger import DBLogger from cpl.database.db_logger import DBLogger
@@ -869,6 +868,8 @@ class DataAccessObjectABC(ABC, Generic[T_DBM]):
async def _get_editor_id(obj: T_DBM): async def _get_editor_id(obj: T_DBM):
editor_id = obj.editor_id editor_id = obj.editor_id
if editor_id is None: if editor_id is None:
from cpl.core.ctx.user_context import get_user
user = get_user() user = get_user()
if user is not None: if user is not None:
editor_id = user.id editor_id = user.id

View File

@@ -4,5 +4,4 @@ from abc import ABC, abstractmethod
class DataSeederABC(ABC): class DataSeederABC(ABC):
@abstractmethod @abstractmethod
async def seed(self): async def seed(self): ...
pass

View File

@@ -2,75 +2,23 @@ from typing import Optional
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl.core.environment import Environment
from cpl.core.utils import Base64
class DatabaseSettings(ConfigurationModelABC): class DatabaseSettings(ConfigurationModelABC):
r"""Represents settings for the database connection"""
def __init__( def __init__(
self, self,
host: str = Environment.get("DB_HOST", str), src: Optional[dict] = None,
port: int = Environment.get("DB_PORT", str, Configuration.get("DB_DEFAULT_PORT", 0)),
user: str = Environment.get("DB_USER", str),
password: str = Environment.get("DB_PASSWORD", str),
database: str = Environment.get("DB_DATABASE", str),
charset: str = Environment.get("DB_CHARSET", str, "utf8mb4"),
use_unicode: bool = Environment.get("DB_USE_UNICODE", bool, False),
buffered: bool = Environment.get("DB_BUFFERED", bool, False),
auth_plugin: str = Environment.get("DB_AUTH_PLUGIN", str, "caching_sha2_password"),
ssl_disabled: bool = Environment.get("DB_SSL_DISABLED", bool, False),
): ):
ConfigurationModelABC.__init__(self) ConfigurationModelABC.__init__(self, src, "DB")
self._host: Optional[str] = host self.option("host", str, required=True)
self._port: Optional[int] = port self.option("port", int, Configuration.get("DB_DEFAULT_PORT"), required=True)
self._user: Optional[str] = user self.option("user", str, required=True)
self._password: Optional[str] = Base64.decode(password) if Base64.is_b64(password) else password self.option("password", str, required=True)
self._database: Optional[str] = database self.option("database", str, required=True)
self._charset: Optional[str] = charset self.option("charset", str, "utf8mb4")
self._use_unicode: Optional[bool] = use_unicode self.option("use_unicode", bool, False)
self._buffered: Optional[bool] = buffered self.option("buffered", bool, False)
self._auth_plugin: Optional[str] = auth_plugin self.option("auth_plugin", str, "caching_sha2_password")
self._ssl_disabled: Optional[bool] = ssl_disabled self.option("ssl_disabled", bool, False)
@property
def host(self) -> Optional[str]:
return self._host
@property
def port(self) -> Optional[int]:
return self._port
@property
def user(self) -> Optional[str]:
return self._user
@property
def password(self) -> Optional[str]:
return self._password
@property
def database(self) -> Optional[str]:
return self._database
@property
def charset(self) -> Optional[str]:
return self._charset
@property
def use_unicode(self) -> Optional[bool]:
return self._use_unicode
@property
def buffered(self) -> Optional[bool]:
return self._buffered
@property
def auth_plugin(self) -> Optional[str]:
return self._auth_plugin
@property
def ssl_disabled(self) -> Optional[bool]:
return self._ssl_disabled

View File

@@ -5,8 +5,7 @@ from mysql.connector.abstracts import MySQLConnectionAbstract
from mysql.connector.cursor import MySQLCursorBuffered from mysql.connector.cursor import MySQLCursorBuffered
from cpl.database.abc.connection_abc import ConnectionABC from cpl.database.abc.connection_abc import ConnectionABC
from cpl.database.database_settings import DatabaseSettings from cpl.database.model.database_settings import DatabaseSettings
from cpl.core.utils.credential_manager import CredentialManager
class DatabaseConnection(ConnectionABC): class DatabaseConnection(ConnectionABC):
@@ -31,7 +30,7 @@ class DatabaseConnection(ConnectionABC):
host=settings.host, host=settings.host,
port=settings.port, port=settings.port,
user=settings.user, user=settings.user,
passwd=CredentialManager.decrypt(settings.password), passwd=settings.password,
charset=settings.charset, charset=settings.charset,
use_unicode=settings.use_unicode, use_unicode=settings.use_unicode,
buffered=settings.buffered, buffered=settings.buffered,
@@ -43,7 +42,7 @@ class DatabaseConnection(ConnectionABC):
host=settings.host, host=settings.host,
port=settings.port, port=settings.port,
user=settings.user, user=settings.user,
passwd=CredentialManager.decrypt(settings.password), passwd=settings.password,
db=settings.database, db=settings.database,
charset=settings.charset, charset=settings.charset,
use_unicode=settings.use_unicode, use_unicode=settings.use_unicode,

View File

@@ -4,8 +4,7 @@ from abc import ABC, abstractmethod
class ScopeABC(ABC): class ScopeABC(ABC):
r"""ABC for the class :class:`cpl.dependency.scope.Scope`""" r"""ABC for the class :class:`cpl.dependency.scope.Scope`"""
def __init__(self): def __init__(self): ...
pass
@property @property
@abstractmethod @abstractmethod

View File

@@ -77,7 +77,7 @@ class ServiceProvider(ServiceProviderABC):
return implementations return implementations
def _build_by_signature(self, sig: Signature, origin_service_type: type) -> list[R]: def _build_by_signature(self, sig: Signature, origin_service_type: type=None) -> list[R]:
params = [] params = []
for param in sig.parameters.items(): for param in sig.parameters.items():
parameter = param[1] parameter = param[1]

View File

@@ -1,6 +1,6 @@
import functools import functools
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from inspect import Signature, signature from inspect import Signature, signature, iscoroutinefunction
from typing import Optional, Type from typing import Optional, Type
from cpl.core.typing import T, R from cpl.core.typing import T, R
@@ -13,8 +13,7 @@ class ServiceProviderABC(ABC):
_provider: Optional["ServiceProviderABC"] = None _provider: Optional["ServiceProviderABC"] = None
@abstractmethod @abstractmethod
def __init__(self): def __init__(self): ...
pass
@classmethod @classmethod
def set_global_provider(cls, provider: "ServiceProviderABC"): def set_global_provider(cls, provider: "ServiceProviderABC"):
@@ -37,8 +36,7 @@ class ServiceProviderABC(ABC):
return cls._provider.get_services(instance_type, *args, **kwargs) return cls._provider.get_services(instance_type, *args, **kwargs)
@abstractmethod @abstractmethod
def _build_by_signature(self, sig: Signature, origin_service_type: type) -> list[R]: def _build_by_signature(self, sig: Signature, origin_service_type: type=None) -> list[R]: ...
pass
@abstractmethod @abstractmethod
def _build_service(self, service_type: type, *args, **kwargs) -> object: def _build_service(self, service_type: type, *args, **kwargs) -> object:
@@ -117,11 +115,13 @@ class ServiceProviderABC(ABC):
return functools.partial(cls.inject) return functools.partial(cls.inject)
@functools.wraps(f) @functools.wraps(f)
def inner(*args, **kwargs): async def inner(*args, **kwargs):
if cls._provider is None: if cls._provider is None:
raise Exception(f"{cls.__name__} not build!") raise Exception(f"{cls.__name__} not build!")
injection = [x for x in cls._provider._build_by_signature(signature(f)) if x is not None] injection = [x for x in cls._provider._build_by_signature(signature(f)) if x is not None]
if iscoroutinefunction(f):
return await f(*args, *injection, **kwargs)
return f(*args, *injection, **kwargs) return f(*args, *injection, **kwargs)
return inner return inner

View File

@@ -2,7 +2,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_client_settings_name_enum import EMailClientSettingsNameEnum
from .email_model import EMail from .email_model import EMail
from .mail_logger import MailLogger from .mail_logger import MailLogger

View File

@@ -2,7 +2,6 @@ import ssl
from smtplib import SMTP from smtplib import SMTP
from typing import Optional from typing import Optional
from cpl.core.utils.credential_manager import CredentialManager
from cpl.mail.abc.email_client_abc import EMailClientABC from cpl.mail.abc.email_client_abc import EMailClientABC
from cpl.mail.email_client_settings import EMailClientSettings from cpl.mail.email_client_settings import EMailClientSettings
from cpl.mail.email_model import EMail from cpl.mail.email_model import EMail
@@ -62,9 +61,7 @@ class EMailClient(EMailClientABC):
__name__, __name__,
f"Try to login {self._mail_settings.user_name}@{self._mail_settings.host}:{self._mail_settings.port}", f"Try to login {self._mail_settings.user_name}@{self._mail_settings.host}:{self._mail_settings.port}",
) )
self._server.login( self._server.login(self._mail_settings.user_name, self._mail_settings.credentials)
self._mail_settings.user_name, CredentialManager.decrypt(self._mail_settings.credentials)
)
self._logger.info( self._logger.info(
__name__, __name__,
f"Logged on as {self._mail_settings.user_name} to {self._mail_settings.host}:{self._mail_settings.port}", f"Logged on as {self._mail_settings.user_name} to {self._mail_settings.host}:{self._mail_settings.port}",

View File

@@ -1,51 +1,17 @@
from typing import Optional
from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC
class EMailClientSettings(ConfigurationModelABC): class EMailClientSettings(ConfigurationModelABC):
r"""Representation of mailing settings"""
def __init__( def __init__(
self, self,
host: str = None, src: Optional[dict] = None,
port: int = None,
user_name: str = None,
credentials: str = None,
): ):
ConfigurationModelABC.__init__(self) ConfigurationModelABC.__init__(self, src, "EMAIL")
self._host: str = host self.option("host", str, required=True)
self._port: int = port self.option("port", int, 587, required=True)
self._user_name: str = user_name self.option("user_name", str, required=True)
self._credentials: str = credentials self.option("credentials", str, required=True)
@property
def host(self) -> str:
return self._host
@host.setter
def host(self, host: str) -> None:
self._host = host
@property
def port(self) -> int:
return self._port
@port.setter
def port(self, port: int) -> None:
self._port = port
@property
def user_name(self) -> str:
return self._user_name
@user_name.setter
def user_name(self, user_name: str) -> None:
self._user_name = user_name
@property
def credentials(self) -> str:
return self._credentials
@credentials.setter
def credentials(self, credentials: str) -> None:
self._credentials = credentials

View File

@@ -1,8 +0,0 @@
from enum import Enum
class EMailClientSettingsNameEnum(Enum):
host = "Host"
port = "Port"
user_name = "UserName"
credentials = "Credentials"

View File

@@ -5,25 +5,19 @@ from cpl.translation.translation_settings import TranslationSettings
class TranslationServiceABC(ABC): class TranslationServiceABC(ABC):
@abstractmethod @abstractmethod
def __init__(self): def __init__(self): ...
pass
@abstractmethod @abstractmethod
def set_default_lang(self, lang: str): def set_default_lang(self, lang: str): ...
pass
@abstractmethod @abstractmethod
def set_lang(self, lang: str): def set_lang(self, lang: str): ...
pass
@abstractmethod @abstractmethod
def load(self, lang: str): def load(self, lang: str): ...
pass
@abstractmethod @abstractmethod
def load_by_settings(self, settings: TranslationSettings): def load_by_settings(self, settings: TranslationSettings): ...
pass
@abstractmethod @abstractmethod
def translate(self, key: str) -> str: def translate(self, key: str) -> str: ...
pass

View File

@@ -0,0 +1,23 @@
from starlette.responses import JSONResponse
from cpl.api.web_app import WebApp
from cpl.application import ApplicationBuilder
from custom.api.src.service import PingService
def main():
builder = ApplicationBuilder[WebApp](WebApp)
builder.services.add_logging()
builder.services.add_transient(PingService)
app = builder.build()
app.with_route(path="/route1", fn=lambda r: JSONResponse("route1"), method="GET")
app.with_routes_directory("routes")
app.with_logging()
app.run()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,13 @@
from urllib.request import Request
from starlette.responses import JSONResponse
from cpl.api.router import Router
from cpl.core.log import Logger
from custom.api.src.service import PingService
@Router.get(f"/ping")
async def ping(r: Request, ping: PingService, logger: Logger):
logger.info(f"Ping: {ping}")
return JSONResponse(ping.ping(r))

View File

@@ -0,0 +1,4 @@
class PingService:
def ping(self, r):
return "pong"

View File

@@ -1,15 +0,0 @@
{
"TimeFormatSettings": {
"DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
},
"LoggingSettings": {
"Path": "logs/",
"Filename": "log_$start_time.log",
"ConsoleLogLevel": "ERROR",
"FileLogLevel": "WARN"
}
}

View File

@@ -1,9 +0,0 @@
{
"WorkspaceSettings": {
"DefaultProject": "async",
"Projects": {
"async": "src/async/async.json"
},
"Scripts": {}
}
}

View File

@@ -1,15 +0,0 @@
from cpl.application import ApplicationABC
from cpl.core.configuration import ConfigurationABC
from cpl.core.console import Console
from cpl.dependency import ServiceProviderABC
class Application(ApplicationABC):
def __init__(self, config: ConfigurationABC, services: ServiceProviderABC):
ApplicationABC.__init__(self, config, services)
async def configure(self):
pass
async def main(self):
Console.write_line("Hello World")

View File

@@ -1,41 +0,0 @@
{
"ProjectSettings": {
"Name": "async",
"Version": {
"Major": "0",
"Minor": "0",
"Micro": "0"
},
"Author": "",
"AuthorEmail": "",
"Description": "",
"LongDescription": "",
"URL": "",
"CopyrightDate": "",
"CopyrightName": "",
"LicenseName": "",
"LicenseDescription": "",
"Dependencies": [
"sh_cpl>=2021.10.0.post1"
],
"PythonVersion": ">=3.9.2",
"PythonPath": {},
"Classifiers": []
},
"BuildSettings": {
"ProjectType": "console",
"SourcePath": "",
"OutputPath": "../../dist",
"Main": "async.main",
"EntryPoint": "async",
"IncludePackageData": false,
"Included": [],
"Excluded": [
"*/__pycache__",
"*/logs",
"*/tests"
],
"PackageData": {},
"ProjectReferences": []
}
}

View File

@@ -1,17 +0,0 @@
import asyncio
from cpl.application import ApplicationBuilder
from application import Application
from startup import Startup
async def main():
app_builder = ApplicationBuilder(Application)
app_builder.use_startup(Startup)
app = await app_builder.build_async()
await app.run_async()
if __name__ == "__main__":
loop = asyncio.new_event_loop()
loop.run_until_complete(main())

View File

@@ -1,17 +0,0 @@
from cpl.application.async_startup_abc import AsyncStartupABC
from cpl.core.configuration import ConfigurationABC
from cpl.dependency import ServiceProviderABC, ServiceCollection
from cpl.core.environment import Environment
class Startup(AsyncStartupABC):
def __init__(self):
AsyncStartupABC.__init__(self)
async def configure_configuration(
self, configuration: ConfigurationABC, environment: Environment
) -> ConfigurationABC:
return configuration
async def configure_services(self, services: ServiceCollection, environment: Environment) -> ServiceProviderABC:
return services.build()

View File

@@ -15,32 +15,27 @@ def test_console():
Console.write_line("Hello World") Console.write_line("Hello World")
Console.write("\nName: ") Console.write("\nName: ")
Console.write_line(" Hello", Console.read_line()) Console.write_line(" Hello", Console.read_line())
Console.clear()
Console.write_at(5, 5, "at 5, 5") Console.write_at(5, 5, "at 5, 5")
Console.write_at(10, 10, "at 10, 10") Console.write_at(10, 10, "at 10, 10")
if __name__ == "__main__": if __name__ == "__main__":
Console.write_line("Hello World\n") Console.write_line("Hello World\n")
Console.clear()
Console.spinner( Console.spinner(
"Test:", test_spinner, spinner_foreground_color=ForegroundColorEnum.cyan, text_foreground_color="green" "Test:", test_spinner, spinner_foreground_color=ForegroundColorEnum.cyan, text_foreground_color="green"
) )
test_console()
Console.write_line("HOLD BACK") Console.write_line("HOLD BACK")
# opts = [ opts = ["Option 1", "Option 2", "Option 3", "Option 4"]
# 'Option 1', selected = Console.select(
# 'Option 2', ">",
# 'Option 3', "Select item:",
# 'Option 4' opts,
# ] header_foreground_color=ForegroundColorEnum.blue,
# selected = Console.select( option_foreground_color=ForegroundColorEnum.green,
# '>', cursor_foreground_color=ForegroundColorEnum.red,
# 'Select item:', )
# opts, Console.write_line(f"You selected: {selected}")
# header_foreground_color=ForegroundColorEnum.blue,
# option_foreground_color=ForegroundColorEnum.green, Console.write_line()
# cursor_foreground_color=ForegroundColorEnum.red
# )
# Console.write_line(f'You selected: {selected}')
# # test_console()
#
# Console.write_line()

View File

@@ -1,5 +1,5 @@
{ {
"ProjectSettings": { "Project": {
"Name": "database", "Name": "database",
"Version": { "Version": {
"Major": "0", "Major": "0",
@@ -22,7 +22,7 @@
"PythonPath": {}, "PythonPath": {},
"Classifiers": [] "Classifiers": []
}, },
"BuildSettings": { "Build": {
"ProjectType": "console", "ProjectType": "console",
"SourcePath": "src", "SourcePath": "src",
"OutputPath": "dist", "OutputPath": "dist",

View File

@@ -1,5 +1,5 @@
from cpl.application.abc.application_abc import ApplicationABC from cpl.application.abc import ApplicationABC
from cpl.auth import KeycloakAdmin from cpl.auth.keycloak import KeycloakAdmin
from cpl.core.console import Console 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
@@ -14,7 +14,7 @@ class Application(ApplicationABC):
def __init__(self, services: ServiceProviderABC): def __init__(self, services: ServiceProviderABC):
ApplicationABC.__init__(self, services) ApplicationABC.__init__(self, services)
self._logger: LoggerABC = services.get_service(LoggerABC) self._logger = services.get_service(LoggerABC)
async def test_daos(self): async def test_daos(self):
userDao: UserDao = self._services.get_service(UserDao) userDao: UserDao = self._services.get_service(UserDao)

View File

@@ -1,8 +1,8 @@
{ {
"LoggingSettings": { "Logging": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "TRACE", "ConsoleLevel": "TRACE",
"FileLogLevel": "TRACE" "Level": "TRACE"
} }
} }

View File

@@ -1,19 +1,19 @@
{ {
"TimeFormatSettings": { "TimeFormat": {
"DateFormat": "%Y-%m-%d", "DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S", "TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
}, },
"LoggingSettings": { "Log": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "TRACE", "ConsoleLevel": "TRACE",
"FileLogLevel": "TRACE" "Level": "TRACE"
}, },
"DatabaseSettings": { "Database": {
"AuthPlugin": "mysql_native_password", "AuthPlugin": "mysql_native_password",
"ConnectionString": "mysql+mysqlconnector://cpl:$credentials@localhost/cpl", "ConnectionString": "mysql+mysqlconnector://cpl:$credentials@localhost/cpl",
"Credentials": "Y3Bs", "Credentials": "Y3Bs",

View File

@@ -1,23 +1,23 @@
{ {
"TimeFormatSettings": { "TimeFormat": {
"DateFormat": "%Y-%m-%d", "DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S", "TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
}, },
"LoggingSettings": { "Log": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "TRACE", "ConsoleLevel": "TRACE",
"FileLogLevel": "TRACE" "Level": "TRACE"
}, },
"DatabaseSettings": { "Database": {
"Host": "localhost", "Host": "localhost",
"User": "cpl", "User": "cpl",
"Port": 3306, "Port": 3306,
"Password": "Y3Bs", "Password": "cpl",
"Database": "cpl", "Database": "cpl",
"Charset": "utf8mb4", "Charset": "utf8mb4",
"UseUnicode": "true", "UseUnicode": "true",

View File

@@ -1,15 +1,15 @@
{ {
"TimeFormatSettings": { "TimeFormat": {
"DateFormat": "%Y-%m-%d", "DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S", "TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
}, },
"LoggingSettings": { "Log": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "ERROR", "ConsoleLevel": "ERROR",
"FileLogLevel": "WARN" "Level": "WARNING"
} }
} }

View File

@@ -1,11 +1,17 @@
from application import Application from application import Application
from cpl.application import ApplicationBuilder from cpl.application import ApplicationBuilder
from custom_permissions import CustomPermissions
from startup import Startup from startup import Startup
def main(): def main():
builder = ApplicationBuilder(Application).use_startup(Startup) builder = ApplicationBuilder(Application).with_startup(Startup)
app = builder.build() app = builder.build()
app.with_logging()
app.with_permissions(CustomPermissions)
app.with_migrations("./scripts")
app.with_seeders()
app.run() app.run()

View File

@@ -1,12 +1,24 @@
from application import Application from application import Application
from cpl.application import ApplicationBuilder from cpl.application import ApplicationBuilder
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 custom_permissions import CustomPermissions
from startup import Startup from startup import Startup
def main(): def main():
builder = ApplicationBuilder(Application).use_startup(Startup) builder = ApplicationBuilder(Application).with_startup(Startup)
builder.services.add_logging()
app = builder.build() app = builder.build()
app.with_logging(LogLevel.trace)
app.with_permissions(CustomPermissions)
app.with_migrations("./scripts")
app.with_seeders()
Console.write_line(CustomPermissions.test.value in PermissionsRegistry.get())
app.run() app.run()
Console.write_line("Hello from main_simplified.py!") Console.write_line("Hello from main_simplified.py!")

View File

@@ -1,30 +1,26 @@
from cpl import auth from cpl import auth
from cpl.application.abc.startup_abc import StartupABC from cpl.application.abc.startup_abc import StartupABC
from cpl.auth import permission from cpl.auth import permission
from cpl.auth.permission.permissions_registry import PermissionsRegistry
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
from cpl.database.abc.data_access_object_abc import DataAccessObjectABC from cpl.database.abc.data_access_object_abc import DataAccessObjectABC
from cpl.database.service.migration_service import MigrationService
from cpl.database.service.seeder_service import SeederService
from cpl.dependency import ServiceCollection from cpl.dependency import ServiceCollection
from custom_permissions import CustomPermissions
from model.city_dao import CityDao from model.city_dao import CityDao
from model.user_dao import UserDao from model.user_dao import UserDao
class Startup(StartupABC): class Startup(StartupABC):
def __init__(self):
StartupABC.__init__(self)
async def configure_configuration(self): @staticmethod
async def configure_configuration():
Configuration.add_json_file(f"appsettings.json") 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_environment()}.json")
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True) Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
async def configure_services(self, services: ServiceCollection): @staticmethod
async def configure_services(services: ServiceCollection):
services.add_module(mysql) services.add_module(mysql)
services.add_module(auth) services.add_module(auth)
services.add_module(permission) services.add_module(permission)
@@ -33,13 +29,3 @@ class Startup(StartupABC):
services.add_transient(DataAccessObjectABC, CityDao) services.add_transient(DataAccessObjectABC, CityDao)
services.add_singleton(LoggerABC, Logger) services.add_singleton(LoggerABC, Logger)
PermissionsRegistry.with_enum(CustomPermissions)
provider = services.build()
migration_service: MigrationService = provider.get_service(MigrationService)
migration_service.with_directory("./scripts")
await migration_service.migrate()
seeder_service: SeederService = provider.get_service(SeederService)
await seeder_service.seed()

View File

@@ -1,15 +1,15 @@
{ {
"TimeFormatSettings": { "TimeFormat": {
"DateFormat": "%Y-%m-%d", "DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S", "TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
}, },
"LoggingSettings": { "Logging": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "ERROR", "ConsoleLevel": "ERROR",
"FileLogLevel": "WARN" "Level": "WARN"
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"WorkspaceSettings": { "Workspace": {
"DefaultProject": "di", "DefaultProject": "di",
"Projects": { "Projects": {
"di": "src/di/di.json" "di": "src/di/di.json"

View File

@@ -1,5 +1,4 @@
from cpl.application import ApplicationABC from cpl.application.abc import ApplicationABC
from cpl.core.configuration import ConfigurationABC
from cpl.core.console.console import Console from cpl.core.console.console import Console
from cpl.dependency import ServiceProviderABC from cpl.dependency import ServiceProviderABC
from cpl.dependency.scope import Scope from cpl.dependency.scope import Scope
@@ -11,15 +10,14 @@ from di.tester import Tester
class Application(ApplicationABC): class Application(ApplicationABC):
def __init__(self, config: ConfigurationABC, services: ServiceProviderABC): def __init__(self, services: ServiceProviderABC):
ApplicationABC.__init__(self, config, services) ApplicationABC.__init__(self, services)
def _part_of_scoped(self): def _part_of_scoped(self):
ts: TestService = self._services.get_service(TestService) ts: TestService = self._services.get_service(TestService)
ts.run() ts.run()
def configure(self): def configure(self): ...
pass
def main(self): def main(self):
with self._services.create_scope() as scope: with self._services.create_scope() as scope:

View File

@@ -1,5 +1,5 @@
{ {
"ProjectSettings": { "Project": {
"Name": "di", "Name": "di",
"Version": { "Version": {
"Major": "0", "Major": "0",
@@ -25,7 +25,7 @@
"PythonPath": {}, "PythonPath": {},
"Classifiers": [] "Classifiers": []
}, },
"BuildSettings": { "Build": {
"ProjectType": "console", "ProjectType": "console",
"SourcePath": "", "SourcePath": "",
"OutputPath": "../../dist", "OutputPath": "../../dist",

View File

@@ -6,7 +6,7 @@ from di.startup import Startup
def main(): def main():
app_builder = ApplicationBuilder(Application) app_builder = ApplicationBuilder(Application)
app_builder.use_startup(Startup) app_builder.with_startup(Startup)
app_builder.build().run() app_builder.build().run()

View File

@@ -1,12 +1,10 @@
from cpl.application import StartupABC from cpl.application.abc import StartupABC
from cpl.core.configuration import ConfigurationABC
from cpl.dependency import ServiceProviderABC, ServiceCollection from cpl.dependency import ServiceProviderABC, ServiceCollection
from cpl.core.environment import Environment from di.di_tester_service import DITesterService
from di.test1_service import Test1Service from di.test1_service import Test1Service
from di.test2_service import Test2Service from di.test2_service import Test2Service
from di.test_abc import TestABC from di.test_abc import TestABC
from di.test_service import TestService from di.test_service import TestService
from di.di_tester_service import DITesterService
from di.tester import Tester from di.tester import Tester
@@ -14,10 +12,9 @@ class Startup(StartupABC):
def __init__(self): def __init__(self):
StartupABC.__init__(self) StartupABC.__init__(self)
def configure_configuration(self, configuration: ConfigurationABC, environment: Environment) -> ConfigurationABC: def configure_configuration(self): ...
return configuration
def configure_services(self, services: ServiceCollection, environment: Environment) -> ServiceProviderABC: def configure_services(self, services: ServiceCollection) -> ServiceProviderABC:
services.add_scoped(TestService) services.add_scoped(TestService)
services.add_scoped(DITesterService) services.add_scoped(DITesterService)

View File

@@ -1,4 +1,3 @@
from cpl.core.configuration import ConfigurationABC
from cpl.dependency import ServiceProvider, ServiceProviderABC from cpl.dependency import ServiceProvider, ServiceProviderABC
from di.test_service import TestService from di.test_service import TestService
@@ -6,5 +5,5 @@ from di.test_service import TestService
class StaticTest: class StaticTest:
@staticmethod @staticmethod
@ServiceProvider.inject @ServiceProvider.inject
def test(services: ServiceProviderABC, config: ConfigurationABC, t1: TestService): def test(services: ServiceProviderABC, t1: TestService):
t1.run() t1.run()

View File

@@ -6,7 +6,7 @@ from cpl.core.utils.string import String
class TestService: class TestService:
def __init__(self): def __init__(self):
self._name = String.random_string(string.ascii_lowercase, 8) self._name = String.random(8)
def run(self): def run(self):
Console.write_line(f"Im {self._name}") Console.write_line(f"Im {self._name}")

View File

@@ -1,8 +1,8 @@
{ {
"LoggingSettings": { "Logging": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "TRACE", "ConsoleLevel": "TRACE",
"FileLogLevel": "TRACE" "Level": "TRACE"
} }
} }

View File

@@ -1,63 +1,20 @@
{ {
"TimeFormatSettings": { "TimeFormat": {
"DateFormat": "%Y-%m-%d", "DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S", "TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
}, },
"LoggingSettings": { "Logging": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "TRACE", "ConsoleLevel": "TRACE",
"FileLogLevel": "TRACE" "Level": "TRACE"
}, },
"EMailClientSettings": { "EMailClient": {
"Host": "mail.sh-edraft.de", "Host": "mail.sh-edraft.de",
"Port": "587", "Port": "587",
"UserName": "dev-srv@sh-edraft.de", "UserName": "dev-srv@sh-edraft.de",
"Credentials": "RmBOQX1eNFYiYjgsSid3fV1nelc2WA==" "Credentials": "RmBOQX1eNFYiYjgsSid3fV1nelc2WA=="
},
"PublishSettings": {
"SourcePath": "../",
"DistPath": "../../dist",
"Templates": [
{
"TemplatePath": "../../publish_templates/all_template.txt",
"Name": "all",
"Description": "",
"LongDescription": "",
"CopyrightDate": "2020",
"CopyrightName": "sh-edraft.de",
"LicenseName": "MIT",
"LicenseDescription": ", see LICENSE for more details.",
"Title": "",
"Author": "Sven Heidemann",
"Version": {
"Major": 2020,
"Minor": 12,
"Micro": 9
}
},
{
"TemplatePath": "../../publish_templates/all_template.txt",
"Name": "sh_edraft",
"Description": "common python library",
"LongDescription": "Library to share common classes and models used at sh-edraft.de",
"CopyrightDate": "2020",
"CopyrightName": "sh-edraft.de",
"LicenseName": "MIT",
"LicenseDescription": ", see LICENSE for more details.",
"Title": "",
"Author": "Sven Heidemann",
"Version": {
"Major": 2020,
"Minor": 12,
"Micro": 9
}
}
],
"IncludedFiles": [],
"ExcludedFiles": [],
"TemplateEnding": "_template.txt"
} }
} }

View File

@@ -1,26 +1,26 @@
{ {
"TimeFormatSettings": { "TimeFormat": {
"DateFormat": "%Y-%m-%d", "DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S", "TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
}, },
"LoggingSettings": { "Logging": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "TRACE", "ConsoleLevel": "TRACE",
"FileLogLevel": "TRACE" "Level": "TRACE"
}, },
"EMailClientSettings": { "EMailClient": {
"Host": "mail.sh-edraft.de", "Host": "mail.sh-edraft.de",
"Port": "587", "Port": "587",
"UserName": "dev-srv@sh-edraft.de", "UserName": "dev-srv@sh-edraft.de",
"Credentials": "RmBOQX1eNFYiYjgsSid3fV1nelc2WA==" "Credentials": "RmBOQX1eNFYiYjgsSid3fV1nelc2WA=="
}, },
"DatabaseSettings": { "Database": {
"Host": "localhost", "Host": "localhost",
"User": "sh_cpl", "User": "sh_cpl",
"Password": "MHZhc0Y2bjhKc1VUMWV0Qw==", "Password": "MHZhc0Y2bjhKc1VUMWV0Qw==",
@@ -31,7 +31,7 @@
"AuthPlugin": "mysql_native_password" "AuthPlugin": "mysql_native_password"
}, },
"TestSettings": { "Test": {
"Value": 20 "Value": 20
} }
} }

View File

@@ -1,15 +1,15 @@
{ {
"TimeFormatSettings": { "TimeFormat": {
"DateFormat": "%Y-%m-%d", "DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S", "TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
}, },
"LoggingSettings": { "Logging": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "ERROR", "ConsoleLevel": "ERROR",
"FileLogLevel": "WARN" "Level": "WARN"
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"ProjectSettings": { "Project": {
"Name": "general", "Name": "general",
"Version": { "Version": {
"Major": "2021", "Major": "2021",
@@ -30,7 +30,7 @@
}, },
"Classifiers": [] "Classifiers": []
}, },
"BuildSettings": { "Build": {
"ProjectType": "console", "ProjectType": "console",
"SourcePath": "", "SourcePath": "",
"OutputPath": "dist", "OutputPath": "dist",

View File

@@ -7,9 +7,9 @@ from test_startup_extension import TestStartupExtension
def main(): def main():
app_builder = ApplicationBuilder(Application) app_builder = ApplicationBuilder(Application)
app_builder.use_startup(Startup) app_builder.with_startup(Startup)
app_builder.use_extension(TestStartupExtension) app_builder.with_extension(TestStartupExtension)
app_builder.use_extension(TestExtension) app_builder.with_extension(TestExtension)
app_builder.build().run() app_builder.build().run()

View File

@@ -1,22 +1,22 @@
from cpl import mail from cpl import mail
from cpl.application.abc import StartupABC from cpl.application.abc import StartupABC
from cpl.core.configuration import Configuration from cpl.core.configuration import Configuration
from cpl.dependency import ServiceCollection, ServiceProviderABC
from cpl.core.environment import Environment from cpl.core.environment import Environment
from cpl.core.pipes import IPAddressPipe from cpl.core.pipes import IPAddressPipe
from cpl.dependency import ServiceCollection
from test_service import TestService from test_service import TestService
class Startup(StartupABC): class Startup(StartupABC):
def __init__(self):
StartupABC.__init__(self)
def configure_configuration(selft): @staticmethod
def configure_configuration():
Configuration.add_json_file(f"appsettings.json") 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_environment()}.json")
Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True) Configuration.add_json_file(f"appsettings.{Environment.get_host_name()}.json", optional=True)
def configure_services(self, services: ServiceCollection): @staticmethod
def configure_services(services: ServiceCollection):
services.add_logging() services.add_logging()
services.add_module(mail) services.add_module(mail)
services.add_transient(IPAddressPipe) services.add_transient(IPAddressPipe)

View File

@@ -4,8 +4,7 @@ from cpl.dependency import ServiceProviderABC
class TestExtension(ApplicationExtensionABC): class TestExtension(ApplicationExtensionABC):
def __init__(self):
ApplicationExtensionABC.__init__(self)
def run(self, services: ServiceProviderABC): @staticmethod
def run(services: ServiceProviderABC):
Console.write_line("Hello World from App Extension") Console.write_line("Hello World from App Extension")

View File

@@ -1,16 +1,14 @@
from cpl.application.abc import StartupExtensionABC from cpl.application.abc import StartupExtensionABC
from cpl.core.configuration import Configuration
from cpl.core.console import Console from cpl.core.console import Console
from cpl.dependency import ServiceCollection from cpl.dependency import ServiceCollection
from cpl.core.environment import Environment
class TestStartupExtension(StartupExtensionABC): class TestStartupExtension(StartupExtensionABC):
def __init__(self):
StartupExtensionABC.__init__(self)
def configure_configuration(self): @staticmethod
def configure_configuration():
Console.write_line("config") Console.write_line("config")
def configure_services(self, services: ServiceCollection): @staticmethod
def configure_services(services: ServiceCollection):
Console.write_line("services") Console.write_line("services")

View File

@@ -1,5 +1,5 @@
{ {
"WorkspaceSettings": { "Workspace": {
"DefaultProject": "translation", "DefaultProject": "translation",
"Projects": { "Projects": {
"translation": "src/translation/translation.json" "translation": "src/translation/translation.json"

View File

@@ -18,8 +18,7 @@ class Application(ApplicationABC):
self._translation.load_by_settings(config.get_configuration(TranslationSettings)) self._translation.load_by_settings(config.get_configuration(TranslationSettings))
self._translation.set_default_lang("de") self._translation.set_default_lang("de")
def configure(self): def configure(self): ...
pass
def main(self): def main(self):
Console.write_line(self._translate.transform("main.text.hello_world")) Console.write_line(self._translate.transform("main.text.hello_world"))

View File

@@ -1,16 +1,16 @@
{ {
"TimeFormatSettings": { "TimeFormat": {
"DateFormat": "%Y-%m-%d", "DateFormat": "%Y-%m-%d",
"TimeFormat": "%H:%M:%S", "TimeFormat": "%H:%M:%S",
"DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f",
"DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S"
}, },
"LoggingSettings": { "Logging": {
"Path": "logs/", "Path": "logs/",
"Filename": "log_$start_time.log", "Filename": "log_$start_time.log",
"ConsoleLogLevel": "ERROR", "ConsoleLevel": "ERROR",
"FileLogLevel": "WARN" "Level": "WARN"
}, },
"Translation": { "Translation": {

View File

@@ -6,7 +6,7 @@ from translation.startup import Startup
def main(): def main():
app_builder = ApplicationBuilder(Application) app_builder = ApplicationBuilder(Application)
app_builder.use_startup(Startup) app_builder.with_startup(Startup)
app_builder.build().run() app_builder.build().run()

View File

@@ -1,5 +1,5 @@
{ {
"ProjectSettings": { "Project": {
"Name": "translation", "Name": "translation",
"Version": { "Version": {
"Major": "0", "Major": "0",
@@ -25,7 +25,7 @@
"PythonPath": {}, "PythonPath": {},
"Classifiers": [] "Classifiers": []
}, },
"BuildSettings": { "Build": {
"ProjectType": "console", "ProjectType": "console",
"SourcePath": "", "SourcePath": "",
"OutputPath": "../../dist", "OutputPath": "../../dist",

View File

@@ -13,8 +13,7 @@ class Application(ApplicationABC):
def __init__(self, config: ConfigurationABC, services: ServiceProviderABC): def __init__(self, config: ConfigurationABC, services: ServiceProviderABC):
ApplicationABC.__init__(self, config, services) ApplicationABC.__init__(self, config, services)
def configure(self): def configure(self): ...
pass
def main(self): def main(self):
runner = unittest.TextTestRunner() runner = unittest.TextTestRunner()

View File

@@ -1,5 +1,5 @@
{ {
"ProjectSettings": { "Project": {
"Name": "unittests", "Name": "unittests",
"Version": { "Version": {
"Major": "2024", "Major": "2024",
@@ -23,7 +23,7 @@
"Classifiers": [], "Classifiers": [],
"DevDependencies": [] "DevDependencies": []
}, },
"BuildSettings": { "Build": {
"ProjectType": "unittest", "ProjectType": "unittest",
"SourcePath": "", "SourcePath": "",
"OutputPath": "../../dist", "OutputPath": "../../dist",

View File

@@ -2,8 +2,6 @@ from unittests_cli.abc.command_test_case import CommandTestCase
class CustomTestCase(CommandTestCase): class CustomTestCase(CommandTestCase):
def setUp(self): def setUp(self): ...
pass
def test_equal(self): def test_equal(self): ...
pass

Some files were not shown because too many files have changed in this diff Show More