Compare commits

..

15 Commits

Author SHA1 Message Date
58dbd3ed1e Cleanup for mysql
All checks were successful
Build on push / prepare (push) Successful in 8s
Build on push / core (push) Successful in 19s
Build on push / query (push) Successful in 18s
Build on push / translation (push) Successful in 15s
Build on push / mail (push) Successful in 17s
2025-09-16 20:21:33 +02:00
cd7dfaf2b4 Fixed spinner thread & remove log thread
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 19s
Build on push / translation (push) Successful in 18s
Build on push / mail (push) Successful in 18s
2025-09-16 18:41:57 +02:00
8ad3e3bdb4 Removed ConfigModel from_dict
All checks were successful
Build on push / prepare (push) Successful in 8s
Build on push / core (push) Successful in 16s
Build on push / query (push) Successful in 17s
Build on push / mail (push) Successful in 17s
Build on push / translation (push) Successful in 18s
2025-09-16 08:51:56 +02:00
b97bc0a3ed Restructuring
All checks were successful
Build on push / prepare (push) Successful in 8s
Build on push / query (push) Successful in 17s
Build on push / core (push) Successful in 26s
Build on push / translation (push) Successful in 15s
Build on push / mail (push) Successful in 17s
2025-09-16 08:48:07 +02:00
5f25400bcd Updated config & environment 2025-09-16 08:39:00 +02:00
5edabbf8c1 Removed docs from sources
All checks were successful
Build on push / prepare (push) Successful in 10s
Build on push / core (push) Successful in 17s
Build on push / query (push) Successful in 18s
Build on push / translation (push) Successful in 15s
Build on push / mail (push) Successful in 16s
2025-09-15 23:08:17 +02:00
62b6435470 Improved logging closes #180
All checks were successful
Build on push / prepare (push) Successful in 9s
Build on push / query (push) Successful in 17s
Build on push / core (push) Successful in 18s
Build on push / mail (push) Successful in 17s
Build on push / translation (push) Successful in 18s
2025-09-15 23:06:38 +02:00
d9d7802f33 Fixed translation build
All checks were successful
Build on push / prepare (push) Successful in 9s
Build on push / query (push) Successful in 19s
Build on push / core (push) Successful in 20s
Build on push / mail (push) Successful in 15s
Build on push / translation (push) Successful in 18s
2025-09-15 21:08:40 +02:00
25b4ca0696 Added cpl-mail
Some checks failed
Build on push / prepare (push) Successful in 9s
Build on push / core (push) Successful in 18s
Build on push / query (push) Successful in 25s
Build on push / translation (push) Failing after 8s
Build on push / mail (push) Successful in 14s
2025-09-15 20:56:07 +02:00
3b120370b8 Added cpl-mail
All checks were successful
Build on push / prepare (push) Successful in 9s
Build on push / translation (push) Successful in 20s
Build on push / mail (push) Successful in 21s
Build on push / query (push) Successful in 21s
Build on push / core (push) Successful in 21s
2025-09-15 18:38:34 +02:00
aac038ef63 Added cpl-mail
Some checks failed
Build on push / core (push) Failing after 9s
Build on push / mail (push) Has been cancelled
Build on push / translation (push) Has been cancelled
Build on push / prepare (push) Has been cancelled
Build on push / query (push) Has been cancelled
2025-09-15 18:35:36 +02:00
784632a0b4 Updated ci
Some checks failed
Build on push / core (push) Has been cancelled
Build on push / query (push) Has been cancelled
Build on push / translation (push) Has been cancelled
Build on push / prepare (push) Has been cancelled
2025-09-15 18:17:23 +02:00
4719c32457 Updated ci
Some checks failed
Build on push / prepare (push) Successful in 9s
Build on push / query (push) Failing after 18s
Build on push / core (push) Successful in 15s
2025-09-15 16:55:57 +02:00
516fa3fb7e Updated ci
Some checks failed
Build on push / prepare (push) Successful in 8s
Build on push / query (push) Successful in 18s
Build on push / core (push) Failing after 18s
2025-09-15 15:28:09 +02:00
2d2bb86720 First update towards cpl2
Some checks failed
Build on push / prepare (push) Failing after 6s
Build on push / build (push) Has been skipped
2025-09-15 15:02:47 +02:00
521 changed files with 1972 additions and 12728 deletions

View File

@@ -0,0 +1,41 @@
name: Build on push
run-name: Build on push
on:
push:
branches:
- dev
jobs:
prepare:
uses: ./.gitea/workflows/prepare.yaml
with:
version_suffix: 'dev'
secrets: inherit
core:
uses: ./.gitea/workflows/package.yaml
needs: [prepare]
with:
working_directory: src/cpl-core
secrets: inherit
query:
uses: ./.gitea/workflows/package.yaml
needs: [prepare]
with:
working_directory: src/cpl-query
secrets: inherit
translation:
uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core ]
with:
working_directory: src/cpl-translation
secrets: inherit
mail:
uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core ]
with:
working_directory: src/cpl-mail
secrets: inherit

View File

@@ -0,0 +1,39 @@
name: Build on push
run-name: Build on push
on:
push:
branches:
- master
jobs:
prepare:
uses: ./.gitea/workflows/prepare.yaml
secrets: inherit
core:
uses: ./.gitea/workflows/package.yaml
needs: [prepare]
with:
working_directory: src/cpl-core
secrets: inherit
query:
uses: ./.gitea/workflows/package.yaml
needs: [prepare]
with:
working_directory: src/cpl-query
secrets: inherit
translation:
uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core ]
with:
working_directory: src/cpl-translation
secrets: inherit
mail:
uses: ./.gitea/workflows/package.yaml
needs: [ prepare, core ]
with:
working_directory: src/cpl-mail
secrets: inherit

View File

@@ -0,0 +1,65 @@
name: Build Package
run-name: Build Python Package
on:
workflow_call:
inputs:
version_suffix:
description: 'Suffix for version (z.B. "dev", "alpha", "beta")'
required: false
type: string
working_directory:
required: true
type: string
jobs:
build:
runs-on: [ runner ]
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Clone Repository
uses: https://github.com/actions/checkout@v3
with:
token: ${{ secrets.CI_ACCESS_TOKEN }}
- name: Download build version artifact
uses: actions/download-artifact@v3
with:
name: version
- name: Set version
run: |
sed -i -E "s/^version = \".*\"/version = \"$(cat /workspace/sh-edraft.de/cpl/version.txt)\"/" pyproject.toml
echo "Set version to $(cat /workspace/sh-edraft.de/cpl/version.txt)"
cat pyproject.toml
- name: Set pip conf
run: |
cat > .pip.conf <<'EOF'
[global]
extra-index-url = https://git.sh-edraft.de/api/packages/sh-edraft.de/pypi/simple/
EOF
- name: Install Dependencies
run: |
export PIP_CONFIG_FILE=".pip.conf"
pip install build
- name: Build Package
run: |
python -m build --outdir dist
- name: Login to registry git.sh-edraft.de
uses: https://github.com/docker/login-action@v1
with:
registry: git.sh-edraft.de
username: ${{ secrets.CI_USERNAME }}
password: ${{ secrets.CI_ACCESS_TOKEN }}
- name: Push image
run: |
pip install twine
python -m twine upload --repository-url https://git.sh-edraft.de/api/packages/sh-edraft.de/pypi -u ${{ secrets.CI_USERNAME }} -p ${{ secrets.CI_ACCESS_TOKEN }} ./dist/*

View File

@@ -0,0 +1,54 @@
name: Prepare Build
run-name: Prepare Build Version
on:
workflow_call:
inputs:
version_suffix:
description: 'Suffix for version (z.B. "dev", "alpha", "beta")'
required: false
type: string
jobs:
prepare:
runs-on: [ runner ]
container: git.sh-edraft.de/sh-edraft.de/act-runner:latest
steps:
- name: Clone Repository
uses: https://github.com/actions/checkout@v3
with:
token: ${{ secrets.CI_ACCESS_TOKEN }}
- name: Get Date and Build Number
run: |
git fetch --tags
git tag
DATE=$(date +'%Y.%m.%d')
TAG_COUNT=$(git tag -l "${DATE}.*" | wc -l)
BUILD_NUMBER=$(($TAG_COUNT + 1))
VERSION_SUFFIX=${{ inputs.version_suffix }}
if [ -n "$VERSION_SUFFIX" ] && [ "$VERSION_SUFFIX" = "dev" ]; then
BUILD_VERSION="${DATE}.dev${BUILD_NUMBER}"
elif [ -n "$VERSION_SUFFIX" ]; then
BUILD_VERSION="${DATE}.${BUILD_NUMBER}${VERSION_SUFFIX}"
else
BUILD_VERSION="${DATE}.${BUILD_NUMBER}"
fi
echo "$BUILD_VERSION" > version.txt
echo "VERSION $BUILD_VERSION"
- name: Create Git Tag for Build
run: |
git config user.name "ci"
git config user.email "dev@sh-edraft.de"
echo "tag $(cat version.txt)"
git tag $(cat version.txt)
git push origin --tags
- name: Upload build version artifact
uses: actions/upload-artifact@v3
with:
name: version
path: version.txt

2
.pip.conf Normal file
View File

@@ -0,0 +1,2 @@
[global]
extra-index-url = https://git.sh-edraft.de/api/packages/sh-edraft.de/pypi/simple/

View File

@@ -1,151 +0,0 @@
{
"WorkspaceSettings": {
"DefaultProject": "cpl-core",
"Projects": {
"cpl-cli": "src/cpl_cli/cpl-cli.json",
"cpl-core": "src/cpl_core/cpl-core.json",
"cpl-discord": "src/cpl_discord/cpl-discord.json",
"cpl-query": "src/cpl_query/cpl-query.json",
"cpl-translation": "src/cpl_translation/cpl-translation.json",
"set-version": "tools/set_version/set-version.json",
"set-pip-urls": "tools/set_pip_urls/set-pip-urls.json",
"unittests": "unittests/unittests/unittests.json",
"unittests_cli": "unittests/unittests_cli/unittests_cli.json",
"unittests_core": "unittests/unittests_core/unittests_core.json",
"unittests_query": "unittests/unittests_query/unittests_query.json",
"unittests_shared": "unittests/unittests_shared/unittests_shared.json",
"unittests_translation": "unittests/unittests_translation/unittests_translation.json"
},
"Scripts": {
"hello-world": "echo 'Hello World'",
"format": "echo 'Formatting:'; black ./",
"sv": "cpl set-version",
"set-version": "cpl run set-version --dev $ARGS; echo '';",
"spu": "cpl set-pip-urls",
"set-pip-urls": "cpl run set-pip-urls --dev $ARGS; echo '';",
"docs-build": "cpl format; echo 'Build Documentation'; cpl db-core; cpl db-discord; cpl db-query; cpl db-translation; cd docs/; make clean; make html;",
"db-core": "cd docs/; sphinx-apidoc -o source/ ../src/cpl_core; cd ../",
"db-discord": "cd docs/; sphinx-apidoc -o source/ ../src/cpl_discord; cd ../",
"db-query": "cd docs/; sphinx-apidoc -o source/ ../src/cpl_query; cd ../",
"db-translation": "cd docs/; sphinx-apidoc -o source/ ../src/cpl_translation; cd ../",
"db": "cpl docs-build",
"docs-open": "xdg-open $PWD/docs/build/html/index.html &",
"do": "cpl docs-open",
"test": "cpl run unittests",
"pre-build-all": "cpl sv $ARGS; cpl spu $ARGS;",
"build-all": "cpl build-cli; cpl build-core; cpl build-discord; cpl build-query; cpl build-translation; cpl build-set-pip-urls; cpl build-set-version",
"ba": "cpl build-all $ARGS",
"build-cli": "echo 'Build cpl-cli'; cd ./src/cpl_cli; cpl build; cd ../../;",
"build-core": "echo 'Build cpl-core'; cd ./src/cpl_core; cpl build; cd ../../;",
"build-discord": "echo 'Build cpl-discord'; cd ./src/cpl_discord; cpl build; cd ../../;",
"build-query": "echo 'Build cpl-query'; cd ./src/cpl_query; cpl build; cd ../../;",
"build-translation": "echo 'Build cpl-translation'; cd ./src/cpl_translation; cpl build; cd ../../;",
"build-set-pip-urls": "echo 'Build set-pip-urls'; cd ./tools/set_pip_urls; cpl build; cd ../../;",
"build-set-version": "echo 'Build set-version'; cd ./tools/set_version; cpl build; cd ../../;",
"pre-publish-all": "cpl sv $ARGS; cpl spu $ARGS;",
"publish-all": "cpl publish-cli; cpl publish-core; cpl publish-discord; cpl publish-query; cpl publish-translation;",
"pa": "cpl publish-all $ARGS",
"publish-cli": "echo 'Publish cpl-cli'; cd ./src/cpl_cli; cpl publish; cd ../../;",
"publish-core": "echo 'Publish cpl-core'; cd ./src/cpl_core; cpl publish; cd ../../;",
"publish-discord": "echo 'Publish cpl-discord'; cd ./src/cpl_discord; cpl publish; cd ../../;",
"publish-query": "echo 'Publish cpl-query'; cd ./src/cpl_query; cpl publish; cd ../../;",
"publish-translation": "echo 'Publish cpl-translation'; cd ./src/cpl_translation; cpl publish; cd ../../;",
"upload-prod-cli": "echo 'PROD Upload cpl-cli'; cpl upl-prod-cli;",
"upl-prod-cli": "twine upload -r pip.sh-edraft.de dist/cpl-cli/publish/setup/*",
"upload-prod-core": "echo 'PROD Upload cpl-core'; cpl upl-prod-core;",
"upl-prod-core": "twine upload -r pip.sh-edraft.de dist/cpl-core/publish/setup/*",
"upload-prod-discord": "echo 'PROD Upload cpl-discord'; cpl upl-prod-discord;",
"upl-prod-discord": "twine upload -r pip.sh-edraft.de dist/cpl-discord/publish/setup/*",
"upload-prod-query": "echo 'PROD Upload cpl-query'; cpl upl-prod-query;",
"upl-prod-query": "twine upload -r pip.sh-edraft.de dist/cpl-query/publish/setup/*",
"upload-prod-translation": "echo 'PROD Upload cpl-translation'; cpl upl-prod-translation;",
"upl-prod-translation": "twine upload -r pip.sh-edraft.de dist/cpl-translation/publish/setup/*",
"upload-exp-cli": "echo 'EXP Upload cpl-cli'; cpl upl-exp-cli;",
"upl-exp-cli": "twine upload -r pip-exp.sh-edraft.de dist/cpl-cli/publish/setup/*",
"upload-exp-core": "echo 'EXP Upload cpl-core'; cpl upl-exp-core;",
"upl-exp-core": "twine upload -r pip-exp.sh-edraft.de dist/cpl-core/publish/setup/*",
"upload-exp-discord": "echo 'EXP Upload cpl-discord'; cpl upl-exp-discord;",
"upl-exp-discord": "twine upload -r pip-exp.sh-edraft.de dist/cpl-discord/publish/setup/*",
"upload-exp-query": "echo 'EXP Upload cpl-query'; cpl upl-exp-query;",
"upl-exp-query": "twine upload -r pip-exp.sh-edraft.de dist/cpl-query/publish/setup/*",
"upload-exp-translation": "echo 'EXP Upload cpl-translation'; cpl upl-exp-translation;",
"upl-exp-translation": "twine upload -r pip-exp.sh-edraft.de dist/cpl-translation/publish/setup/*",
"upload-dev-cli": "echo 'DEV Upload cpl-cli'; cpl upl-dev-cli;",
"upl-dev-cli": "twine upload -r pip-dev.sh-edraft.de dist/cpl-cli/publish/setup/*",
"upload-dev-core": "echo 'DEV Upload cpl-core'; cpl upl-dev-core;",
"upl-dev-core": "twine upload -r pip-dev.sh-edraft.de dist/cpl-core/publish/setup/*",
"upload-dev-discord": "echo 'DEV Upload cpl-discord'; cpl upl-dev-discord;",
"upl-dev-discord": "twine upload -r pip-dev.sh-edraft.de dist/cpl-discord/publish/setup/*",
"upload-dev-query": "echo 'DEV Upload cpl-query'; cpl upl-dev-query;",
"upl-dev-query": "twine upload -r pip-dev.sh-edraft.de dist/cpl-query/publish/setup/*",
"upload-dev-translation": "echo 'DEV Upload cpl-translation'; cpl upl-dev-translation;",
"upl-dev-translation": "twine upload -r pip-dev.sh-edraft.de dist/cpl-translation/publish/setup/*",
"pre-deploy-prod": "cpl sv $ARGS; cpl spu --environment=production;",
"deploy-prod": "cpl deploy-prod-cli; cpl deploy-prod-core; cpl deploy-prod-discord; cpl deploy-prod-query; cpl deploy-prod-translation;",
"dp": "cpl deploy-prod $ARGS",
"deploy-prod-cli": "cpl publish-cli; cpl upload-prod-cli",
"deploy-prod-core": "cpl publish-core; cpl upload-prod-core",
"deploy-prod-query": "cpl publish-query; cpl upload-prod-query",
"deploy-prod-discord": "cpl publish-discord; cpl upload-prod-discord",
"deploy-prod-translation": "cpl publish-translation; cpl upload-prod-translation",
"pre-deploy-exp": "cpl sv $ARGS; cpl spu --environment=staging;",
"deploy-exp": "cpl deploy-exp-cli; cpl deploy-exp-core; cpl deploy-exp-discord; cpl deploy-exp-query; cpl deploy-exp-translation;",
"de": "cpl deploy-exp $ARGS",
"deploy-exp-cli": "cpl publish-cli; cpl upload-exp-cli",
"deploy-exp-core": "cpl publish-core; cpl upload-exp-core",
"deploy-exp-discord": "cpl publish-discord; cpl upload-exp-discord",
"deploy-exp-query": "cpl publish-query; cpl upload-exp-query",
"deploy-exp-translation": "cpl publish-translation; cpl upload-exp-translation",
"pre-deploy-dev": "cpl sv $ARGS; cpl spu --environment=development;",
"deploy-dev": "cpl deploy-dev-cli; cpl deploy-dev-core; cpl deploy-dev-discord; cpl deploy-dev-query; cpl deploy-dev-translation;",
"dd": "cpl deploy-dev $ARGS",
"deploy-dev-cli": "cpl publish-cli; cpl upload-dev-cli",
"deploy-dev-core": "cpl publish-core; cpl upload-dev-core",
"deploy-dev-discord": "cpl publish-discord; cpl upload-dev-discord",
"deploy-dev-query": "cpl publish-query; cpl upload-dev-query",
"deploy-dev-translation": "cpl publish-query; cpl upload-dev-translation",
"dev-install": "cpl di-core; cpl di-cli; cpl di-query; cpl di-translation;",
"di": "cpl dev-install",
"di-core": "pip install cpl-core --pre --upgrade --extra-index-url https://pip-dev.sh-edraft.de",
"di-cli": "pip install cpl-cli --pre --upgrade --extra-index-url https://pip-dev.sh-edraft.de",
"di-discord": "pip install cpl-discord --pre --upgrade --extra-index-url https://pip-dev.sh-edraft.de",
"di-query": "pip install cpl-query --pre --upgrade --extra-index-url https://pip-dev.sh-edraft.de",
"di-translation": "pip install cpl-translation --pre --upgrade --extra-index-url https://pip-dev.sh-edraft.de",
"prod-install": "cpl pi-core; cpl pi-cli; cpl pi-query; cpl pi-translation;",
"pi": "cpl prod-install",
"pi-core": "pip install cpl-core --pre --upgrade --extra-index-url https://pip.sh-edraft.de",
"pi-cli": "pip install cpl-cli --pre --upgrade --extra-index-url https://pip.sh-edraft.de",
"pi-discord": "pip install cpl-discord --pre --upgrade --extra-index-url https://pip.sh-edraft.de",
"pi-query": "pip install cpl-query --pre --upgrade --extra-index-url https://pip.sh-edraft.de",
"pi-translation": "pip install cpl-translation --pre --upgrade --extra-index-url https://pip.sh-edraft.de"
}
}
}

View File

@@ -0,0 +1,6 @@
from .application_abc import ApplicationABC
from .application_builder import ApplicationBuilder
from .application_builder_abc import ApplicationBuilderABC
from .application_extension_abc import ApplicationExtensionABC
from .startup_abc import StartupABC
from .startup_extension_abc import StartupExtensionABC

View File

@@ -1,26 +1,23 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional from typing import Optional
from cpl_core.configuration.configuration_abc import ConfigurationABC from cpl.dependency.service_provider_abc import ServiceProviderABC
from cpl_core.console.console import Console
from cpl_core.dependency_injection.service_provider_abc import ServiceProviderABC from cpl.core.console.console import Console
from cpl_core.environment.application_environment_abc import ApplicationEnvironmentABC
class ApplicationABC(ABC): class ApplicationABC(ABC):
r"""ABC for the Application class r"""ABC for the Application class
Parameters: Parameters:
config: :class:`cpl_core.configuration.configuration_abc.ConfigurationABC` config: :class:`cpl.core.configuration.configuration_abc.ConfigurationABC`
Contains object loaded from appsettings Contains object loaded from appsettings
services: :class:`cpl_core.dependency_injection.service_provider_abc.ServiceProviderABC` services: :class:`cpl.dependency.service_provider_abc.ServiceProviderABC`
Contains instances of prepared objects Contains instances of prepared objects
""" """
@abstractmethod @abstractmethod
def __init__(self, config: ConfigurationABC, services: ServiceProviderABC): def __init__(self, services: ServiceProviderABC):
self._configuration: Optional[ConfigurationABC] = config
self._environment: Optional[ApplicationEnvironmentABC] = self._configuration.environment
self._services: Optional[ServiceProviderABC] = services self._services: Optional[ServiceProviderABC] = services
def run(self): def run(self):
@@ -49,14 +46,12 @@ class ApplicationABC(ABC):
def configure(self): def configure(self):
r"""Configure the application r"""Configure the application
Called by :class:`cpl_core.application.application_abc.ApplicationABC.run` Called by :class:`cpl.application.application_abc.ApplicationABC.run`
""" """
pass
@abstractmethod @abstractmethod
def main(self): def main(self):
r"""Custom entry point r"""Custom entry point
Called by :class:`cpl_core.application.application_abc.ApplicationABC.run` Called by :class:`cpl.application.application_abc.ApplicationABC.run`
""" """
pass

View File

@@ -0,0 +1,97 @@
from typing import Type, Optional, Callable, Union
from cpl.application.application_abc import ApplicationABC
from cpl.application.application_builder_abc import ApplicationBuilderABC
from cpl.application.application_extension_abc import ApplicationExtensionABC
from cpl.application.async_application_extension_abc import AsyncApplicationExtensionABC
from cpl.application.async_startup_abc import AsyncStartupABC
from cpl.application.async_startup_extension_abc import AsyncStartupExtensionABC
from cpl.application.startup_abc import StartupABC
from cpl.application.startup_extension_abc import StartupExtensionABC
from cpl.core.configuration.configuration import Configuration
from cpl.dependency.service_collection import ServiceCollection
from cpl.core.environment import Environment
class ApplicationBuilder(ApplicationBuilderABC):
r"""This is class is used to build an object of :class:`cpl.application.application_abc.ApplicationABC`
Parameter:
app: Type[:class:`cpl.application.application_abc.ApplicationABC`]
Application to build
"""
def __init__(self, app: Type[ApplicationABC]):
ApplicationBuilderABC.__init__(self)
self._app = app
self._startup: Optional[StartupABC | AsyncStartupABC] = None
self._services = ServiceCollection()
self._app_extensions: list[Type[ApplicationExtensionABC | AsyncApplicationExtensionABC]] = []
self._startup_extensions: list[Type[StartupExtensionABC | AsyncStartupABC]] = []
def use_startup(self, startup: Type[StartupABC | AsyncStartupABC]) -> "ApplicationBuilder":
self._startup = startup()
return self
def use_extension(
self,
extension: Type[
ApplicationExtensionABC | AsyncApplicationExtensionABC | StartupExtensionABC | AsyncStartupExtensionABC
],
) -> "ApplicationBuilder":
if (
issubclass(extension, ApplicationExtensionABC) or issubclass(extension, AsyncApplicationExtensionABC)
) and extension not in self._app_extensions:
self._app_extensions.append(extension)
elif (
issubclass(extension, StartupExtensionABC) or issubclass(extension, AsyncStartupExtensionABC)
) and extension not in self._startup_extensions:
self._startup_extensions.append(extension)
return self
def _build_startup(self):
for ex in self._startup_extensions:
extension = ex()
extension.configure_configuration(Configuration, Environment)
extension.configure_services(self._services, Environment)
if self._startup is not None:
self._startup.configure_configuration(Configuration, Environment)
self._startup.configure_services(self._services, Environment)
async def _build_async_startup(self):
for ex in self._startup_extensions:
extension = ex()
await extension.configure_configuration(Configuration, Environment)
await extension.configure_services(self._services, Environment)
if self._startup is not None:
await self._startup.configure_configuration(Configuration, Environment)
await self._startup.configure_services(self._services, Environment)
def build(self) -> ApplicationABC:
self._build_startup()
config = Configuration
services = self._services.build_service_provider()
for ex in self._app_extensions:
extension = ex()
extension.run(config, services)
return self._app(services)
async def build_async(self) -> ApplicationABC:
await self._build_async_startup()
config = Configuration
services = self._services.build_service_provider()
for ex in self._app_extensions:
extension = ex()
await extension.run(config, services)
return self._app(services)

View File

@@ -1,12 +1,12 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Type from typing import Type
from cpl_core.application.application_abc import ApplicationABC from cpl.application.application_abc import ApplicationABC
from cpl_core.application.startup_abc import StartupABC from cpl.application.startup_abc import StartupABC
class ApplicationBuilderABC(ABC): class ApplicationBuilderABC(ABC):
r"""ABC for the :class:`cpl_core.application.application_builder.ApplicationBuilder`""" r"""ABC for the :class:`cpl.application.application_builder.ApplicationBuilder`"""
@abstractmethod @abstractmethod
def __init__(self, *args): def __init__(self, *args):
@@ -17,35 +17,31 @@ class ApplicationBuilderABC(ABC):
r"""Sets the custom startup class to use r"""Sets the custom startup class to use
Parameter: Parameter:
startup: Type[:class:`cpl_core.application.startup_abc.StartupABC`] startup: Type[:class:`cpl.application.startup_abc.StartupABC`]
Startup class to use Startup class to use
""" """
pass
@abstractmethod @abstractmethod
async def use_startup(self, startup: Type[StartupABC]): async def use_startup(self, startup: Type[StartupABC]):
r"""Sets the custom startup class to use async r"""Sets the custom startup class to use async
Parameter: Parameter:
startup: Type[:class:`cpl_core.application.startup_abc.StartupABC`] startup: Type[:class:`cpl.application.startup_abc.StartupABC`]
Startup class to use Startup class to use
""" """
pass
@abstractmethod @abstractmethod
def build(self) -> ApplicationABC: def build(self) -> ApplicationABC:
r"""Creates custom application object r"""Creates custom application object
Returns: Returns:
Object of :class:`cpl_core.application.application_abc.ApplicationABC` Object of :class:`cpl.application.application_abc.ApplicationABC`
""" """
pass
@abstractmethod @abstractmethod
async def build_async(self) -> ApplicationABC: async def build_async(self) -> ApplicationABC:
r"""Creates custom application object async r"""Creates custom application object async
Returns: Returns:
Object of :class:`cpl_core.application.application_abc.ApplicationABC` Object of :class:`cpl.application.application_abc.ApplicationABC`
""" """
pass

View File

@@ -0,0 +1,14 @@
from abc import ABC, abstractmethod
from cpl.core.configuration.configuration import Configuration
from cpl.dependency import ServiceProviderABC
class ApplicationExtensionABC(ABC):
@abstractmethod
def __init__(self):
pass
@abstractmethod
def run(self, config: Configuration, services: ServiceProviderABC):
pass

View File

@@ -0,0 +1,14 @@
from abc import ABC, abstractmethod
from cpl.core.configuration.configuration import Configuration
from cpl.dependency import ServiceProviderABC
class AsyncApplicationExtensionABC(ABC):
@abstractmethod
def __init__(self):
pass
@abstractmethod
async def run(self, config: Configuration, services: ServiceProviderABC):
pass

View File

@@ -0,0 +1,23 @@
from abc import ABC, abstractmethod
from cpl.dependency.service_collection import ServiceCollection
class AsyncStartupABC(ABC):
r"""ABC for the startup class"""
@abstractmethod
def __init__(self):
pass
@abstractmethod
async def configure_configuration(self):
r"""Creates configuration of application"""
@abstractmethod
async def configure_services(self, service: ServiceCollection):
r"""Creates service provider
Parameter:
services: :class:`cpl.dependency.service_collection`
"""

View File

@@ -0,0 +1,31 @@
from abc import ABC, abstractmethod
from cpl.core.configuration.configuration import Configuration
from cpl.dependency.service_collection import ServiceCollection
from cpl.core.environment.environment import Environment
class AsyncStartupExtensionABC(ABC):
r"""ABC for startup extension classes"""
@abstractmethod
def __init__(self):
pass
@abstractmethod
async def configure_configuration(self, config: Configuration, env: Environment):
r"""Creates configuration of application
Parameter:
config: :class:`cpl.core.configuration.configuration_abc.Configuration`
env: :class:`cpl.core.environment.application_environment_abc`
"""
@abstractmethod
async def configure_services(self, service: ServiceCollection, env: Environment):
r"""Creates service provider
Parameter:
services: :class:`cpl.dependency.service_collection`
env: :class:`cpl.core.environment.application_environment_abc`
"""

View File

@@ -0,0 +1,31 @@
from abc import ABC, abstractmethod
from cpl.core.configuration import Configuration
from cpl.dependency.service_collection import ServiceCollection
from cpl.core.environment import Environment
class StartupABC(ABC):
r"""ABC for the startup class"""
@abstractmethod
def __init__(self):
pass
@abstractmethod
def configure_configuration(self, config: Configuration, env: Environment):
r"""Creates configuration of application
Parameter:
config: :class:`cpl.core.configuration.configuration_abc.ConfigurationABC`
env: :class:`cpl.core.environment.application_environment_abc`
"""
@abstractmethod
def configure_services(self, service: ServiceCollection, env: Environment):
r"""Creates service provider
Parameter:
services: :class:`cpl.dependency.service_collection`
env: :class:`cpl.core.environment.application_environment_abc`
"""

View File

@@ -0,0 +1,33 @@
from abc import ABC, abstractmethod
from cpl.core.configuration import Configuration
from cpl.dependency.service_collection import ServiceCollection
from cpl.core.environment.environment import Environment
class StartupExtensionABC(ABC):
r"""ABC for startup extension classes"""
@abstractmethod
def __init__(self):
pass
@abstractmethod
def configure_configuration(self, config: Configuration, env: Environment):
r"""Creates configuration of application
Parameter:
config: :class:`cpl.core.configuration.configuration_abc.ConfigurationABC`
env: :class:`cpl.core.environment.application_environment_abc`
"""
@abstractmethod
def configure_services(self, service: ServiceCollection, env: Environment):
r"""Creates service provider
Parameter:
services: :class:`cpl.dependency.service_collection`
env: :class:`cpl.core.environment.application_environment_abc`
"""

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-application"
version = "2024.7.0"
description = "CPL application"
readme ="CPL application package"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [
{ name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" }
]
keywords = ["cpl", "application", "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,2 @@
cpl-core
cpl-dependency

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
from .configuration import Configuration
from .configuration_model_abc import ConfigurationModelABC

View File

@@ -0,0 +1,134 @@
import inspect
import json
import os
import sys
from typing import Any
from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC
from cpl.core.console.console import Console
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.utils.json_processor import JSONProcessor
class Configuration:
_config = {}
@staticmethod
def _print_info(message: str):
r"""Prints an info message
Parameter:
name: :class:`str`
Info name
message: :class:`str`
Info message
"""
Console.set_foreground_color(ForegroundColorEnum.green)
Console.write_line(f"[CONFIG] {message}")
Console.set_foreground_color(ForegroundColorEnum.default)
@staticmethod
def _print_warn(message: str):
r"""Prints a warning
Parameter:
name: :class:`str`
Warning name
message: :class:`str`
Warning message
"""
Console.set_foreground_color(ForegroundColorEnum.yellow)
Console.write_line(f"[CONFIG] {message}")
Console.set_foreground_color(ForegroundColorEnum.default)
@staticmethod
def _print_error(message: str):
r"""Prints an error
Parameter:
name: :class:`str`
Error name
message: :class:`str`
Error message
"""
Console.set_foreground_color(ForegroundColorEnum.red)
Console.write_line(f"[CONFIG] {message}")
Console.set_foreground_color(ForegroundColorEnum.default)
@classmethod
def _load_json_file(cls, file: str, output: bool) -> dict:
r"""Reads the json file
Parameter:
file: :class:`str`
Name of the file
output: :class:`bool`
Specifies whether an output should take place
Returns:
Object of :class:`dict`
"""
try:
# open config file, create if not exists
with open(file, encoding="utf-8") as cfg:
# load json
json_cfg = json.load(cfg)
if output:
cls._print_info(f"Loaded config file: {file}")
return json_cfg
except Exception as e:
cls._print_error(f"Cannot load config file: {file}! -> {e}")
return {}
@classmethod
def add_json_file(cls, name: str, optional: bool = None, output: bool = True, path: str = None):
if os.path.isabs(name):
file_path = name
else:
path_root = Environment.get_cwd()
if path is not None:
path_root = path
if str(path_root).endswith("/") and not name.startswith("/"):
file_path = f"{path_root}{name}"
else:
file_path = f"{path_root}/{name}"
if not os.path.isfile(file_path):
if optional is not True:
if output:
cls._print_error(f"File not found: {file_path}")
sys.exit()
if output:
cls._print_warn(f"Not Loaded config file: {file_path}")
return None
config_from_file = cls._load_json_file(file_path, output)
for sub in ConfigurationModelABC.__subclasses__():
for key, value in config_from_file.items():
if sub.__name__ != key and sub.__name__.replace("Settings", "") != key:
continue
configuration = JSONProcessor.process(sub, value)
cls.set(sub, configuration)
@classmethod
def set(cls, key: Any, value: T):
if inspect.isclass(key):
key = key.__name__
cls._config[key] = value
@classmethod
def get(cls, key: Any, default: D = None) -> T | D:
if inspect.isclass(key):
key = key.__name__
return cls._config.get(key, default)

View File

@@ -0,0 +1,5 @@
from abc import ABC
class ConfigurationModelABC(ABC):
pass

View File

@@ -0,0 +1,4 @@
from .background_color_enum import BackgroundColorEnum
from .console import Console
from ._call import ConsoleCall
from .foreground_color_enum import ForegroundColorEnum

View File

@@ -1,28 +1,29 @@
import os import os
import sys import sys
import threading import multiprocessing
import time import time
from multiprocessing import Process
from termcolor import colored from termcolor import colored
from cpl_core.console.background_color_enum import BackgroundColorEnum from cpl.core.console.background_color_enum import BackgroundColorEnum
from cpl_core.console.foreground_color_enum import ForegroundColorEnum from cpl.core.console.foreground_color_enum import ForegroundColorEnum
class SpinnerThread(threading.Thread): class Spinner(Process):
r"""Thread to show spinner in terminal r"""Process to show spinner in terminal
Parameter: Parameter:
msg_len: :class:`int` msg_len: :class:`int`
Length of the message Length of the message
foreground_color: :class:`cpl_core.console.foreground_color.ForegroundColorEnum` foreground_color: :class:`cpl.core.console.foreground_color.ForegroundColorEnum`
Foreground color of the spinner Foreground color of the spinner
background_color: :class:`cpl_core.console.background_color.BackgroundColorEnum` background_color: :class:`cpl.core.console.background_color.BackgroundColorEnum`
Background color of the spinner Background color of the spinner
""" """
def __init__(self, msg_len: int, foreground_color: ForegroundColorEnum, background_color: BackgroundColorEnum): def __init__(self, msg_len: int, foreground_color: ForegroundColorEnum, background_color: BackgroundColorEnum):
threading.Thread.__init__(self) Process.__init__(self)
self._msg_len = msg_len self._msg_len = msg_len
self._foreground_color = foreground_color self._foreground_color = foreground_color
@@ -50,29 +51,26 @@ class SpinnerThread(threading.Thread):
return color_args return color_args
def run(self) -> None: def run(self) -> None:
r"""Entry point of thread, shows the spinner""" r"""Entry point of process, shows the spinner"""
columns = 0 columns = 0
if sys.platform == "win32": if sys.platform == "win32":
columns = os.get_terminal_size().columns columns = os.get_terminal_size().columns
else: else:
term_rows, term_columns = os.popen("stty size", "r").read().split() values = os.popen("stty size", "r").read().split()
term_rows, term_columns = values if len(values) == 2 else (0, 0)
columns = int(term_columns) columns = int(term_columns)
end_msg = "done" end_msg = "done"
end_msg_pos = columns - self._msg_len - len(end_msg)
if end_msg_pos > 0: padding = columns - self._msg_len - len(end_msg)
print(f'{"" : >{end_msg_pos}}', end="") if padding > 0:
print(f'{"" : >{padding}}', end="")
else: else:
print("", end="") print("", end="")
first = True
spinner = self._spinner() spinner = self._spinner()
while self._is_spinning: while self._is_spinning:
if first: print(colored(f"{next(spinner): >{len(end_msg)}}", *self._get_color_args()), end="")
first = False
print(colored(f"{next(spinner): >{len(end_msg) - 1}}", *self._get_color_args()), end="")
else:
print(colored(f"{next(spinner): >{len(end_msg)}}", *self._get_color_args()), end="")
time.sleep(0.1) time.sleep(0.1)
back = "" back = ""
for i in range(0, len(end_msg)): for i in range(0, len(end_msg)):
@@ -84,9 +82,10 @@ class SpinnerThread(threading.Thread):
if not self._exit: if not self._exit:
print(colored(end_msg, *self._get_color_args()), end="") print(colored(end_msg, *self._get_color_args()), end="")
def stop_spinning(self): def stop(self):
r"""Stops the spinner""" r"""Stops the spinner"""
self._is_spinning = False self._is_spinning = False
super().terminate()
time.sleep(0.1) time.sleep(0.1)
def exit(self): def exit(self):

View File

@@ -9,14 +9,15 @@ import colorama
from tabulate import tabulate from tabulate import tabulate
from termcolor import colored from termcolor import colored
from cpl_core.console.background_color_enum import BackgroundColorEnum from cpl.core.console.background_color_enum import BackgroundColorEnum
from cpl_core.console.console_call import ConsoleCall from cpl.core.console._call import ConsoleCall
from cpl_core.console.foreground_color_enum import ForegroundColorEnum from cpl.core.console.foreground_color_enum import ForegroundColorEnum
from cpl_core.console.spinner_thread import SpinnerThread from cpl.core.console._spinner import Spinner
class Console: class Console:
r"""Useful functions for handling with input and output""" r"""Useful functions for handling with input and output"""
colorama.init() colorama.init()
_is_first_write = True _is_first_write = True
@@ -61,7 +62,7 @@ class Console:
r"""Sets the background color r"""Sets the background color
Parameter: Parameter:
color: Union[:class:`cpl_core.console.background_color_enum.BackgroundColorEnum`, :class:`str`] color: Union[:class:`cpl.core.console.background_color_enum.BackgroundColorEnum`, :class:`str`]
Background color of the console Background color of the console
""" """
if type(color) is str: if type(color) is str:
@@ -74,7 +75,7 @@ class Console:
r"""Sets the foreground color r"""Sets the foreground color
Parameter: Parameter:
color: Union[:class:`cpl_core.console.background_color_enum.BackgroundColorEnum`, :class:`str`] color: Union[:class:`cpl.core.console.background_color_enum.BackgroundColorEnum`, :class:`str`]
Foreground color of the console Foreground color of the console
""" """
if type(color) is str: if type(color) is str:
@@ -365,17 +366,17 @@ class Console:
Message or header of the selection Message or header of the selection
options: List[:class:`str`] options: List[:class:`str`]
Selectable options Selectable options
header_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`] header_foreground_color: Union[:class:`str`, :class:`cpl.core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the header Foreground color of the header
header_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`] header_background_color: Union[:class:`str`, :class:`cpl.core.console.background_color_enum.BackgroundColorEnum`]
Background color of the header Background color of the header
option_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`] option_foreground_color: Union[:class:`str`, :class:`cpl.core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the options Foreground color of the options
option_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`] option_background_color: Union[:class:`str`, :class:`cpl.core.console.background_color_enum.BackgroundColorEnum`]
Background color of the options Background color of the options
cursor_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`] cursor_foreground_color: Union[:class:`str`, :class:`cpl.core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the cursor Foreground color of the cursor
cursor_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`] cursor_background_color: Union[:class:`str`, :class:`cpl.core.console.background_color_enum.BackgroundColorEnum`]
Background color of the cursor Background color of the cursor
Returns: Returns:
@@ -429,13 +430,13 @@ class Console:
Function to call Function to call
args: :class:`list` args: :class:`list`
Arguments of the function Arguments of the function
text_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`] text_foreground_color: Union[:class:`str`, :class:`cpl.core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the text Foreground color of the text
spinner_foreground_color: Union[:class:`str`, :class:`cpl_core.console.foreground_color_enum.ForegroundColorEnum`] spinner_foreground_color: Union[:class:`str`, :class:`cpl.core.console.foreground_color_enum.ForegroundColorEnum`]
Foreground color of the spinner Foreground color of the spinner
text_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`] text_background_color: Union[:class:`str`, :class:`cpl.core.console.background_color_enum.BackgroundColorEnum`]
Background color of the text Background color of the text
spinner_background_color: Union[:class:`str`, :class:`cpl_core.console.background_color_enum.BackgroundColorEnum`] spinner_background_color: Union[:class:`str`, :class:`cpl.core.console.background_color_enum.BackgroundColorEnum`]
Background color of the spinner Background color of the spinner
kwargs: :class:`dict` kwargs: :class:`dict`
Keyword arguments of the call Keyword arguments of the call
@@ -463,7 +464,7 @@ class Console:
cls.set_hold_back(True) cls.set_hold_back(True)
spinner = None spinner = None
if not cls._disabled: if not cls._disabled:
spinner = SpinnerThread(len(message), spinner_foreground_color, spinner_background_color) spinner = Spinner(len(message), spinner_foreground_color, spinner_background_color)
spinner.start() spinner.start()
return_value = None return_value = None
@@ -475,7 +476,7 @@ class Console:
cls.close() cls.close()
if spinner is not None: if spinner is not None:
spinner.stop_spinning() spinner.stop()
cls.set_hold_back(False) cls.set_hold_back(False)
cls.set_foreground_color(ForegroundColorEnum.default) cls.set_foreground_color(ForegroundColorEnum.default)

View File

@@ -0,0 +1,2 @@
from .environment_enum import EnvironmentEnum
from .environment import Environment

View File

@@ -0,0 +1,68 @@
import os
from socket import gethostname
from typing import Optional, Type
from cpl.core.environment.environment_enum import EnvironmentEnum
from cpl.core.typing import T
from cpl.core.utils.get_value import get_value
class Environment:
r"""Represents environment of the application
Parameter:
name: :class:`cpl.core.environment.environment_name_enum.EnvironmentNameEnum`
"""
@classmethod
def get_environment(cls):
return cls.get("ENVIRONMENT", str, EnvironmentEnum.production.value)
@classmethod
def set_environment(cls, environment: str):
assert environment is not None and environment != "", "environment must not be None or empty"
assert environment.lower() in [
e.value for e in EnvironmentEnum
], f"environment must be one of {[e.value for e in EnvironmentEnum]}"
cls.set("ENVIRONMENT", environment.lower())
@classmethod
def get_app_name(cls) -> str:
return cls.get("APP_NAME", str)
@classmethod
def set_app_name(cls, app_name: str):
cls.set("APP_NAME", app_name)
@staticmethod
def get_host_name() -> str:
return gethostname()
@staticmethod
def get_cwd() -> str:
return os.getcwd()
@staticmethod
def set_cwd(working_directory: str):
assert working_directory is not None and working_directory != "", "working_directory must not be None or empty"
os.chdir(working_directory)
@staticmethod
def set(key: str, value: T):
assert key is not None and key != "", "key must not be None or empty"
os.environ[key] = str(value)
@staticmethod
def get(key: str, cast_type: Type[T], default: Optional[T] = None) -> Optional[T]:
"""
Get an environment variable and cast it to a specified type.
:param str key: The name of the environment variable.
: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.
:return: The casted value, or None if the variable is not found.
:rtype: Optional[T]
"""
return get_value(dict(os.environ), key, cast_type, default)

View File

@@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
class EnvironmentNameEnum(Enum): class EnvironmentEnum(Enum):
production = "production" production = "production"
staging = "staging" staging = "staging"
testing = "testing" testing = "testing"

View File

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

View File

@@ -0,0 +1,11 @@
from enum import Enum
class LogLevelEnum(Enum):
off = "OFF" # Nothing
trace = "TRC" # Detailed app information's
debug = "DEB" # Detailed app state
info = "INF" # Normal information's
warning = "WAR" # Error that can later be fatal
error = "ERR" # Non fatal error
fatal = "FAT" # Error that cause exit

View File

@@ -0,0 +1,117 @@
import os
import traceback
from datetime import datetime
from cpl.core.console import Console
from cpl.core.log.log_level_enum import LogLevelEnum
from cpl.core.log.logger_abc import LoggerABC
from cpl.core.typing import Messages, Source
class Logger(LoggerABC):
_level = LogLevelEnum.info
_levels = [x for x in LogLevelEnum]
# ANSI color codes for different log levels
_COLORS = {
LogLevelEnum.trace: "\033[37m", # Light Gray
LogLevelEnum.debug: "\033[94m", # Blue
LogLevelEnum.info: "\033[92m", # Green
LogLevelEnum.warning: "\033[93m", # Yellow
LogLevelEnum.error: "\033[91m", # Red
LogLevelEnum.fatal: "\033[95m", # Magenta
}
def __init__(self, source: Source, file_prefix: str = None):
LoggerABC.__init__(self)
assert source is not None and source != "", "Source cannot be None or empty"
self._source = source
if file_prefix is None:
file_prefix = "app"
self._file_prefix = file_prefix
self._create_log_dir()
@property
def log_file(self):
return f"logs/{self._file_prefix}_{datetime.now().strftime('%Y-%m-%d')}.log"
@staticmethod
def _create_log_dir():
if os.path.exists("logs"):
return
os.makedirs("logs")
@classmethod
def set_level(cls, level: LogLevelEnum):
if level in cls._levels:
cls._level = level
else:
raise ValueError(f"Invalid log level: {level}")
@staticmethod
def _ensure_file_size(log_file: str):
if not os.path.exists(log_file) or os.path.getsize(log_file) <= 0.5 * 1024 * 1024:
return
# if exists and size is greater than 300MB, create a new file
os.rename(
log_file,
f"{log_file.split('.log')[0]}_{datetime.now().strftime('%H-%M-%S')}.log",
)
def _write_log_to_file(self, content: str):
file = self.log_file
self._ensure_file_size(file)
with open(file, "a") as log_file:
log_file.write(content + "\n")
log_file.close()
def _log(self, level: LogLevelEnum, *messages: Messages):
try:
if self._levels.index(level) < self._levels.index(self._level):
return
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
formatted_message = self._format_message(level.value, timestamp, *messages)
self._write_log_to_file(formatted_message)
Console.write_line(f"{self._COLORS.get(self._level, '\033[0m')}{formatted_message}\033[0m")
except Exception as e:
print(f"Error while logging: {e} -> {traceback.format_exc()}")
def _format_message(self, level: str, timestamp, *messages: Messages) -> str:
if isinstance(messages, tuple):
messages = list(messages)
if not isinstance(messages, list):
messages = [messages]
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)}"
def header(self, string: str):
self._log(LogLevelEnum.info, string)
def trace(self, *messages: Messages):
self._log(LogLevelEnum.trace, *messages)
def debug(self, *messages: Messages):
self._log(LogLevelEnum.debug, *messages)
def info(self, *messages: Messages):
self._log(LogLevelEnum.info, *messages)
def warning(self, *messages: Messages):
self._log(LogLevelEnum.warning, *messages)
def error(self, message, e: Exception = None):
self._log(LogLevelEnum.error, message, f"{e} -> {traceback.format_exc()}" if e else None)
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)
if not prevent_quit:
exit(-1)

View File

@@ -1,12 +1,18 @@
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from cpl.core.typing import Messages
class LoggerABC(ABC): class LoggerABC(ABC):
r"""ABC for :class:`cpl_core.logging.logger_service.Logger`""" r"""ABC for :class:`cpl.core.log.logger_service.Logger`"""
@abstractmethod @abstractmethod
def __init__(self): def set_level(self, level: str):
ABC.__init__(self) pass
@abstractmethod
def _format_message(self, level: str, timestamp, *messages: Messages) -> str:
pass
@abstractmethod @abstractmethod
def header(self, string: str): def header(self, string: str):
@@ -16,10 +22,9 @@ class LoggerABC(ABC):
string: :class:`str` string: :class:`str`
String to write as header String to write as header
""" """
pass
@abstractmethod @abstractmethod
def trace(self, name: str, message: str): def trace(self, *messages: Messages):
r"""Writes a trace message r"""Writes a trace message
Parameter: Parameter:
@@ -28,10 +33,9 @@ class LoggerABC(ABC):
message: :class:`str` message: :class:`str`
Message string Message string
""" """
pass
@abstractmethod @abstractmethod
def debug(self, name: str, message: str): def debug(self, *messages: Messages):
r"""Writes a debug message r"""Writes a debug message
Parameter: Parameter:
@@ -40,10 +44,9 @@ class LoggerABC(ABC):
message: :class:`str` message: :class:`str`
Message string Message string
""" """
pass
@abstractmethod @abstractmethod
def info(self, name: str, message: str): def info(self, *messages: Messages):
r"""Writes an information r"""Writes an information
Parameter: Parameter:
@@ -52,10 +55,9 @@ class LoggerABC(ABC):
message: :class:`str` message: :class:`str`
Message string Message string
""" """
pass
@abstractmethod @abstractmethod
def warn(self, name: str, message: str): def warning(self, *messages: Messages):
r"""Writes an warning r"""Writes an warning
Parameter: Parameter:
@@ -64,10 +66,9 @@ class LoggerABC(ABC):
message: :class:`str` message: :class:`str`
Message string Message string
""" """
pass
@abstractmethod @abstractmethod
def error(self, name: str, message: str, ex: Exception = None): def error(self, messages: str, e: Exception = None):
r"""Writes an error r"""Writes an error
Parameter: Parameter:
@@ -78,10 +79,9 @@ class LoggerABC(ABC):
ex: :class:`Exception` ex: :class:`Exception`
Thrown exception Thrown exception
""" """
pass
@abstractmethod @abstractmethod
def fatal(self, name: str, message: str, ex: Exception = None): def fatal(self, messages: str, e: Exception = None):
r"""Writes an error and ends the program r"""Writes an error and ends the program
Parameter: Parameter:
@@ -92,4 +92,3 @@ class LoggerABC(ABC):
ex: :class:`Exception` ex: :class:`Exception`
Thrown exception Thrown exception
""" """
pass

View File

@@ -1,28 +1,24 @@
import traceback
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.console.console import Console from cpl.core.log.log_level_enum import LogLevelEnum
from cpl_core.console.foreground_color_enum import ForegroundColorEnum
from cpl_core.logging.logging_level_enum import LoggingLevelEnum
from cpl_core.logging.logging_settings_name_enum import LoggingSettingsNameEnum
class LoggingSettings(ConfigurationModelABC): class LogSettings(ConfigurationModelABC):
r"""Representation of logging settings""" r"""Representation of logging settings"""
def __init__( def __init__(
self, self,
path: str = None, path: str = None,
filename: str = None, filename: str = None,
console_log_level: LoggingLevelEnum = None, console_log_level: LogLevelEnum = None,
file_log_level: LoggingLevelEnum = None, file_log_level: LogLevelEnum = None,
): ):
ConfigurationModelABC.__init__(self) ConfigurationModelABC.__init__(self)
self._path: Optional[str] = path self._path: Optional[str] = path
self._filename: Optional[str] = filename self._filename: Optional[str] = filename
self._console: Optional[LoggingLevelEnum] = console_log_level self._console: Optional[LogLevelEnum] = console_log_level
self._level: Optional[LoggingLevelEnum] = file_log_level self._level: Optional[LogLevelEnum] = file_log_level
@property @property
def path(self) -> str: def path(self) -> str:
@@ -41,17 +37,17 @@ class LoggingSettings(ConfigurationModelABC):
self._filename = filename self._filename = filename
@property @property
def console(self) -> LoggingLevelEnum: def console(self) -> LogLevelEnum:
return self._console return self._console
@console.setter @console.setter
def console(self, console: LoggingLevelEnum) -> None: def console(self, console: LogLevelEnum) -> None:
self._console = console self._console = console
@property @property
def level(self) -> LoggingLevelEnum: def level(self) -> LogLevelEnum:
return self._level return self._level
@level.setter @level.setter
def level(self, level: LoggingLevelEnum) -> None: def level(self, level: LogLevelEnum) -> None:
self._level = level self._level = level

View File

@@ -0,0 +1,3 @@
from .bool_pipe import BoolPipe
from .ip_address_pipe import IPAddressPipe
from .pipe_abc import PipeABC

View File

@@ -0,0 +1,13 @@
from cpl.core.pipes.pipe_abc import PipeABC
from cpl.core.typing import T
class BoolPipe[bool](PipeABC):
@staticmethod
def to_str(value: T, *args):
return str(value).lower()
@staticmethod
def from_str(value: str, *args) -> T:
return value in ("True", "true", "1", "yes", "y", "Y")

View File

@@ -0,0 +1,38 @@
from cpl.core.pipes.pipe_abc import PipeABC
from cpl.core.typing import T
class IPAddressPipe[list](PipeABC):
@staticmethod
def to_str(value: T, *args) -> str:
string = ""
if len(value) != 4:
raise ValueError("Invalid IP")
for i in range(0, len(value)):
byte = value[i]
if not 0 <= byte <= 255:
raise ValueError("Invalid IP")
if i == len(value) - 1:
string += f"{byte}"
else:
string += f"{byte}."
return string
@staticmethod
def from_str(value: str, *args) -> T:
parts = value.split(".")
if len(parts) != 4:
raise Exception("Invalid IP")
result = []
for part in parts:
byte = int(part)
if not 0 <= byte <= 255:
raise Exception("Invalid IP")
result.append(byte)
return result

View File

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

View File

@@ -0,0 +1,2 @@
from .time_format_settings import TimeFormatSettings
from .time_format_settings_names_enum import TimeFormatSettingsNamesEnum

View File

@@ -1,10 +1,6 @@
import traceback
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.console.console import Console
from cpl_core.console.foreground_color_enum import ForegroundColorEnum
from cpl_core.time.time_format_settings_names_enum import TimeFormatSettingsNamesEnum
class TimeFormatSettings(ConfigurationModelABC): class TimeFormatSettings(ConfigurationModelABC):

View File

@@ -0,0 +1,10 @@
from typing import TypeVar, Any
T = TypeVar("T")
D = TypeVar("D")
R = TypeVar("R")
Service = TypeVar("Service")
Source = TypeVar("Source")
Messages = list[Any] | Any

View File

@@ -0,0 +1,5 @@
from .b64 import B64
from .credential_manager import CredentialManager
from .json_processor import JSONProcessor
from .pip import Pip
from .string import String

View File

@@ -0,0 +1,43 @@
import base64
from typing import Union
class B64:
@staticmethod
def encode(string: str) -> str:
"""
Encode a string with base64
:param string:
:return:
"""
return base64.b64encode(string.encode("utf-8")).decode("utf-8")
@staticmethod
def decode(string: str) -> str:
"""
Decode a string with base64
:param string:
:return:
"""
return base64.b64decode(string).decode("utf-8")
@staticmethod
def is_b64(sb: Union[str, bytes]) -> bool:
"""
Check if a string is base64 encoded
:param Union[str, bytes] sb:
:return:
:rtype: bool
"""
try:
if isinstance(sb, str):
# If there's any unicode here, an exception will be thrown and the function will return false
sb_bytes = bytes(sb, "ascii")
elif isinstance(sb, bytes):
sb_bytes = sb
else:
raise ValueError("Argument must be string or bytes")
return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes
except ValueError:
return False

View File

@@ -0,0 +1,56 @@
from typing import Type, Optional
from cpl.core.typing import T
def get_value(
source: dict,
key: str,
cast_type: Type[T],
default: Optional[T] = None,
list_delimiter: str = ",",
) -> Optional[T]:
"""
Get value from source dictionary and cast it to a specified type.
:param dict source: The source dictionary.
:param str key: The name of the environment variable.
: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.
:param str list_delimiter: The delimiter to split the value into a list. Defaults to ",".
:return: The casted value, or None if the key is not found.
:rtype: Optional[T]
"""
if key not in source:
return default
value = source[key]
if isinstance(
value,
cast_type if not hasattr(cast_type, "__origin__") else cast_type.__origin__,
):
# Handle list[int] case explicitly
if hasattr(cast_type, "__origin__") and cast_type.__origin__ == list:
subtype = cast_type.__args__[0] if hasattr(cast_type, "__args__") else None
if subtype is not None:
return [subtype(item) for item in value]
return value
try:
if cast_type == bool:
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):
return default

View File

@@ -1,7 +1,7 @@
import enum import enum
from inspect import signature, Parameter from inspect import signature, Parameter
from cpl_core.utils import String from cpl.core.utils.string import String
class JSONProcessor: class JSONProcessor:
@@ -16,7 +16,7 @@ class JSONProcessor:
if parameter.name == "self" or parameter.annotation == Parameter.empty: if parameter.name == "self" or parameter.annotation == Parameter.empty:
continue continue
name = String.first_to_upper(String.convert_to_camel_case(parameter.name)) name = String.first_to_upper(String.to_camel_case(parameter.name))
name_first_lower = String.first_to_lower(name) name_first_lower = String.first_to_lower(name)
if name in values or name_first_lower in values or name.upper() in values: if name in values or name_first_lower in values or name.upper() in values:
value = "" value = ""

View File

@@ -4,11 +4,10 @@ import sys
from contextlib import suppress from contextlib import suppress
from typing import Optional from typing import Optional
from cpl_core.console import Console
class Pip: class Pip:
r"""Executes pip commands""" r"""Executes pip commands"""
_executable = sys.executable _executable = sys.executable
_env = os.environ _env = os.environ

View File

@@ -1,13 +1,13 @@
import random
import re import re
import string import string
import random
class String: class String:
r"""Useful functions for strings""" r"""Useful functions for strings"""
@staticmethod @staticmethod
def convert_to_camel_case(chars: str) -> str: def to_camel_case(s: str) -> str:
r"""Converts string to camel case r"""Converts string to camel case
Parameter: Parameter:
@@ -17,16 +17,10 @@ class String:
Returns: Returns:
String converted to CamelCase String converted to CamelCase
""" """
converted_name = chars return re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
char_set = string.punctuation + " "
for char in char_set:
if char in converted_name:
converted_name = "".join(word.title() for word in converted_name.split(char))
return converted_name
@staticmethod @staticmethod
def convert_to_snake_case(chars: str) -> str: def to_snake_case(chars: str) -> str:
r"""Converts string to snake case r"""Converts string to snake case
Parameter: Parameter:
@@ -56,7 +50,7 @@ class String:
return re.sub(pattern2, r"\1_\2", file_name).lower() return re.sub(pattern2, r"\1_\2", file_name).lower()
@staticmethod @staticmethod
def first_to_upper(chars: str) -> str: def first_to_upper(s: str) -> str:
r"""Converts first char to upper r"""Converts first char to upper
Parameter: Parameter:
@@ -66,10 +60,10 @@ class String:
Returns: Returns:
String with first char as upper String with first char as upper
""" """
return f"{chars[0].upper()}{chars[1:]}" return s[0].upper() + s[1:] if s else s
@staticmethod @staticmethod
def first_to_lower(chars: str) -> str: def first_to_lower(s: str) -> str:
r"""Converts first char to lower r"""Converts first char to lower
Parameter: Parameter:
@@ -79,14 +73,24 @@ class String:
Returns: Returns:
String with first char as lower String with first char as lower
""" """
return f"{chars[0].lower()}{chars[1:]}" return s[0].lower() + s[1:] if s else s
@staticmethod @staticmethod
def random_string(chars: str, length: int) -> str: def random(length: int, letters=True, digits=False, special_characters=False) -> str:
r"""Creates random string by given chars and length r"""Creates random string by given chars and length
Returns: Returns:
String of random chars String of random chars
""" """
return "".join(random.choice(chars) for _ in range(length)) characters = []
if letters:
characters.append(string.ascii_letters)
if digits:
characters.append(string.digits)
if special_characters:
characters.append(string.punctuation)
return "".join(random.choice(characters) for _ in range(length)) if characters else ""

View File

@@ -0,0 +1,29 @@
[build-system]
requires = ["setuptools>=70.1.0", "wheel>=0.43.0"]
build-backend = "setuptools.build_meta"
[project]
name = "cpl-core"
version = "2024.7.0"
description = "CPL core"
readme = "CPL core package"
requires-python = ">=3.12"
license = "MIT"
authors = [
{ name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" }
]
keywords = ["cpl", "core", "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 @@
art==6.5
colorama==0.4.6
tabulate==0.9.0
termcolor==3.1.0
mysql-connector-python==9.4.0
pynput==1.8.1

View File

@@ -0,0 +1,23 @@
from cpl.dependency import ServiceCollection as _ServiceCollection
from . import mysql
from .database_settings import DatabaseSettings
from .database_settings_name_enum import DatabaseSettingsNameEnum
from .mysql.context import DatabaseContextABC, DatabaseContext
from .table_abc import TableABC
def add_mysql(collection: _ServiceCollection):
from cpl.core.console import Console
from cpl.core.configuration import Configuration
try:
collection.add_singleton(DatabaseContextABC, DatabaseContext)
database_context = collection.build_service_provider().get_service(DatabaseContextABC)
db_settings: DatabaseSettings = Configuration.get(DatabaseSettings)
database_context.connect(db_settings)
except ImportError as e:
Console.error("cpl-translation is not installed", str(e))
_ServiceCollection.with_module(add_mysql, mysql.__name__)

View File

@@ -1,6 +1,6 @@
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
class DatabaseSettings(ConfigurationModelABC): class DatabaseSettings(ConfigurationModelABC):

View File

@@ -0,0 +1,8 @@
from cpl.core.log import Logger
from cpl.core.typing import Source
class DBLogger(Logger):
def __init__(self, source: Source):
Logger.__init__(self, source, "db")

View File

@@ -0,0 +1,2 @@
from .database_connection import DatabaseConnection
from .database_connection_abc import DatabaseConnectionABC

View File

@@ -4,9 +4,9 @@ import mysql.connector as sql
from mysql.connector.abstracts import MySQLConnectionAbstract from mysql.connector.abstracts import MySQLConnectionAbstract
from mysql.connector.cursor import MySQLCursorBuffered from mysql.connector.cursor import MySQLCursorBuffered
from cpl_core.database.connection.database_connection_abc import DatabaseConnectionABC from cpl.database.mysql.connection.database_connection_abc import DatabaseConnectionABC
from cpl_core.database.database_settings import DatabaseSettings from cpl.database.database_settings import DatabaseSettings
from cpl_core.utils.credential_manager import CredentialManager from cpl.core.utils.credential_manager import CredentialManager
class DatabaseConnection(DatabaseConnectionABC): class DatabaseConnection(DatabaseConnectionABC):

View File

@@ -1,12 +1,12 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from cpl_core.database.database_settings import DatabaseSettings from cpl.database.database_settings import DatabaseSettings
from mysql.connector.abstracts import MySQLConnectionAbstract from mysql.connector.abstracts import MySQLConnectionAbstract
from mysql.connector.cursor import MySQLCursorBuffered from mysql.connector.cursor import MySQLCursorBuffered
class DatabaseConnectionABC(ABC): class DatabaseConnectionABC(ABC):
r"""ABC for the :class:`cpl_core.database.connection.database_connection.DatabaseConnection`""" r"""ABC for the :class:`cpl.database.connection.database_connection.DatabaseConnection`"""
@abstractmethod @abstractmethod
def __init__(self): def __init__(self):
@@ -30,4 +30,3 @@ class DatabaseConnectionABC(ABC):
connection_string: :class:`str` connection_string: :class:`str`
Database connection string, see: https://docs.sqlalchemy.org/en/14/core/engines.html Database connection string, see: https://docs.sqlalchemy.org/en/14/core/engines.html
""" """
pass

View File

@@ -0,0 +1,2 @@
from .database_context import DatabaseContext
from .database_context_abc import DatabaseContextABC

View File

@@ -1,12 +1,10 @@
from typing import Optional from typing import Optional
import mysql
from cpl_core.database.connection.database_connection import DatabaseConnection from cpl.database.mysql.connection.database_connection import DatabaseConnection
from cpl_core.database.connection.database_connection_abc import DatabaseConnectionABC from cpl.database.mysql.connection.database_connection_abc import DatabaseConnectionABC
from cpl_core.database.context.database_context_abc import DatabaseContextABC from cpl.database.mysql.context.database_context_abc import DatabaseContextABC
from cpl_core.database.database_settings import DatabaseSettings from cpl.database.database_settings import DatabaseSettings
from cpl_core.database.table_abc import TableABC
from mysql.connector.cursor import MySQLCursorBuffered from mysql.connector.cursor import MySQLCursorBuffered
@@ -14,7 +12,7 @@ class DatabaseContext(DatabaseContextABC):
r"""Representation of the database context r"""Representation of the database context
Parameter: Parameter:
database_settings: :class:`cpl_core.database.database_settings.DatabaseSettings` database_settings: :class:`cpl.database.database_settings.DatabaseSettings`
""" """
def __init__(self): def __init__(self):
@@ -31,7 +29,7 @@ class DatabaseContext(DatabaseContextABC):
def _ping_and_reconnect(self): def _ping_and_reconnect(self):
try: try:
self._db.server.ping(reconnect=True, attempts=3, delay=5) self._db.server.ping(reconnect=True, attempts=3, delay=5)
except Exception as err: except Exception:
# reconnect your cursor as you did in __init__ or wherever # reconnect your cursor as you did in __init__ or wherever
if self._settings is None: if self._settings is None:
raise Exception("Call DatabaseContext.connect first") raise Exception("Call DatabaseContext.connect first")

View File

@@ -1,11 +1,11 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from cpl_core.database.database_settings import DatabaseSettings from cpl.database.database_settings import DatabaseSettings
from mysql.connector.cursor import MySQLCursorBuffered from mysql.connector.cursor import MySQLCursorBuffered
class DatabaseContextABC(ABC): class DatabaseContextABC(ABC):
r"""ABC for the :class:`cpl_core.database.context.database_context.DatabaseContext`""" r"""ABC for the :class:`cpl.database.context.database_context.DatabaseContext`"""
@abstractmethod @abstractmethod
def __init__(self, *args): def __init__(self, *args):
@@ -21,14 +21,12 @@ class DatabaseContextABC(ABC):
r"""Connects to a database by connection settings r"""Connects to a database by connection settings
Parameter: Parameter:
database_settings :class:`cpl_core.database.database_settings.DatabaseSettings` database_settings :class:`cpl.database.database_settings.DatabaseSettings`
""" """
pass
@abstractmethod @abstractmethod
def save_changes(self): def save_changes(self):
r"""Saves changes of the database""" r"""Saves changes of the database"""
pass
@abstractmethod @abstractmethod
def select(self, statement: str) -> list[tuple]: def select(self, statement: str) -> list[tuple]:
@@ -40,4 +38,3 @@ class DatabaseContextABC(ABC):
Returns: Returns:
list: Fetched list of selected elements list: Fetched list of selected elements
""" """
pass

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-database"
version = "2024.7.0"
description = "CPL database"
readme ="CPL database package"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [
{ name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" }
]
keywords = ["cpl", "database", "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,2 @@
cpl-core
cpl-dependency

View File

@@ -0,0 +1,7 @@
from .scope import Scope
from .scope_abc import ScopeABC
from .service_collection import ServiceCollection
from .service_descriptor import ServiceDescriptor
from .service_lifetime_enum import ServiceLifetimeEnum
from .service_provider import ServiceProvider
from .service_provider_abc import ServiceProviderABC

View File

@@ -1,6 +1,5 @@
from cpl_core.console.console import Console from cpl.dependency.scope_abc import ScopeABC
from cpl_core.dependency_injection.scope_abc import ScopeABC from cpl.dependency.service_provider_abc import ServiceProviderABC
from cpl_core.dependency_injection.service_provider_abc import ServiceProviderABC
class Scope(ScopeABC): class Scope(ScopeABC):

View File

@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
class ScopeABC(ABC): class ScopeABC(ABC):
r"""ABC for the class :class:`cpl_core.dependency_injection.scope.Scope`""" r"""ABC for the class :class:`cpl.dependency.scope.Scope`"""
def __init__(self): def __init__(self):
pass pass
@@ -13,11 +13,9 @@ class ScopeABC(ABC):
r"""Returns to service provider of scope r"""Returns to service provider of scope
Returns: Returns:
Object of type :class:`cpl_core.dependency_injection.service_provider_abc.ServiceProviderABC` Object of type :class:`cpl.dependency.service_provider_abc.ServiceProviderABC`
""" """
pass
@abstractmethod @abstractmethod
def dispose(self): def dispose(self):
r"""Sets service_provider to None""" r"""Sets service_provider to None"""
pass

View File

@@ -0,0 +1,18 @@
from cpl.dependency.scope import Scope
from cpl.dependency.scope_abc import ScopeABC
from cpl.dependency.service_provider_abc import ServiceProviderABC
class ScopeBuilder:
r"""Class to build :class:`cpl.dependency.scope.Scope`"""
def __init__(self, service_provider: ServiceProviderABC) -> None:
self._service_provider = service_provider
def build(self) -> ScopeABC:
r"""Returns scope
Returns:
Object of type :class:`cpl.dependency.scope.Scope`
"""
return Scope(self._service_provider)

View File

@@ -1,27 +1,26 @@
from typing import Union, Type, Callable, Optional from typing import Union, Type, Callable, Optional
from cpl_core.configuration.configuration_abc import ConfigurationABC from cpl.core.log.logger import Logger
from cpl_core.database.context.database_context_abc import DatabaseContextABC from cpl.core.log.logger_abc import LoggerABC
from cpl_core.database.database_settings import DatabaseSettings from cpl.core.pipes.pipe_abc import PipeABC
from cpl_core.dependency_injection.service_collection_abc import ServiceCollectionABC from cpl.core.typing import T, Service
from cpl_core.dependency_injection.service_descriptor import ServiceDescriptor from cpl.dependency.service_descriptor import ServiceDescriptor
from cpl_core.dependency_injection.service_lifetime_enum import ServiceLifetimeEnum from cpl.dependency.service_lifetime_enum import ServiceLifetimeEnum
from cpl_core.dependency_injection.service_provider import ServiceProvider from cpl.dependency.service_provider import ServiceProvider
from cpl_core.dependency_injection.service_provider_abc import ServiceProviderABC from cpl.dependency.service_provider_abc import ServiceProviderABC
from cpl_core.logging.logger_abc import LoggerABC
from cpl_core.logging.logger_service import Logger
from cpl_core.pipes.pipe_abc import PipeABC
from cpl_core.type import T
class ServiceCollection(ServiceCollectionABC): class ServiceCollection:
r"""Representation of the collection of services""" r"""Representation of the collection of services"""
def __init__(self, config: ConfigurationABC): _modules: dict[str, Callable] = {}
ServiceCollectionABC.__init__(self)
self._configuration: ConfigurationABC = config
self._database_context: Optional[DatabaseContextABC] = None @classmethod
def with_module(cls, func: Callable, name: str = None):
cls._modules[func.__name__ if name is None else name] = func
return cls
def __init__(self):
self._service_descriptors: list[ServiceDescriptor] = [] self._service_descriptors: list[ServiceDescriptor] = []
def _add_descriptor(self, service: Union[type, object], lifetime: ServiceLifetimeEnum, base_type: Callable = None): def _add_descriptor(self, service: Union[type, object], lifetime: ServiceLifetimeEnum, base_type: Callable = None):
@@ -47,13 +46,22 @@ class ServiceCollection(ServiceCollectionABC):
return self return self
def add_db_context(self, db_context_type: Type[DatabaseContextABC], db_settings: DatabaseSettings): def add_module(self, module: str | object):
self.add_singleton(DatabaseContextABC, db_context_type) if not isinstance(module, str):
self._database_context = self.build_service_provider().get_service(DatabaseContextABC) module = module.__name__
self._database_context.connect(db_settings)
if module not in self._modules:
raise ValueError(f"Module {module} not found")
self._modules[module](self)
# def add_mysql(self, db_context_type: Type[DatabaseContextABC], db_settings: DatabaseSettings):
# self.add_singleton(DatabaseContextABC, db_context_type)
# self._database_context = self.build_service_provider().get_service(DatabaseContextABC)
# self._database_context.connect(db_settings)
def add_logging(self): def add_logging(self):
self.add_singleton(LoggerABC, Logger) self.add_transient(LoggerABC, Logger)
return self return self
def add_pipes(self): def add_pipes(self):
@@ -61,19 +69,19 @@ class ServiceCollection(ServiceCollectionABC):
self.add_transient(PipeABC, pipe) self.add_transient(PipeABC, pipe)
return self return self
def add_singleton(self, service_type: T, service: T = None): def add_singleton(self, service_type: T, service: Service = None):
self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.singleton, service) self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.singleton, service)
return self return self
def add_scoped(self, service_type: T, service: T = None): def add_scoped(self, service_type: T, service: Service = None):
self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.scoped, service) self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.scoped, service)
return self return self
def add_transient(self, service_type: T, service: T = None): def add_transient(self, service_type: T, service: Service = None):
self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.transient, service) self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.transient, service)
return self return self
def build_service_provider(self) -> ServiceProviderABC: def build_service_provider(self) -> ServiceProviderABC:
sp = ServiceProvider(self._service_descriptors, self._configuration, self._database_context) sp = ServiceProvider(self._service_descriptors)
ServiceProviderABC.set_global_provider(sp) ServiceProviderABC.set_global_provider(sp)
return sp return sp

View File

@@ -1,7 +1,6 @@
from typing import Union, Optional from typing import Union, Optional
from cpl_core.console import Console from cpl.dependency.service_lifetime_enum import ServiceLifetimeEnum
from cpl_core.dependency_injection.service_lifetime_enum import ServiceLifetimeEnum
class ServiceDescriptor: class ServiceDescriptor:
@@ -10,7 +9,7 @@ class ServiceDescriptor:
Parameter: Parameter:
implementation: Union[:class:`type`, Optional[:class:`object`]] implementation: Union[:class:`type`, Optional[:class:`object`]]
Object or type of service Object or type of service
lifetime: :class:`cpl_core.dependency_injection.service_lifetime_enum.ServiceLifetimeEnum` lifetime: :class:`cpl.dependency.service_lifetime_enum.ServiceLifetimeEnum`
Lifetime of the service Lifetime of the service
""" """

View File

@@ -3,16 +3,15 @@ import typing
from inspect import signature, Parameter, Signature from inspect import signature, Parameter, Signature
from typing import Optional from typing import Optional
from cpl_core.configuration.configuration_abc import ConfigurationABC 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.database.context.database_context_abc import DatabaseContextABC from cpl.core.environment import Environment
from cpl_core.dependency_injection.scope_abc import ScopeABC from cpl.core.typing import T, R, Source
from cpl_core.dependency_injection.scope_builder import ScopeBuilder from cpl.dependency.scope_abc import ScopeABC
from cpl_core.dependency_injection.service_descriptor import ServiceDescriptor from cpl.dependency.scope_builder import ScopeBuilder
from cpl_core.dependency_injection.service_lifetime_enum import ServiceLifetimeEnum from cpl.dependency.service_descriptor import ServiceDescriptor
from cpl_core.dependency_injection.service_provider_abc import ServiceProviderABC from cpl.dependency.service_lifetime_enum import ServiceLifetimeEnum
from cpl_core.environment.application_environment_abc import ApplicationEnvironmentABC from cpl.dependency.service_provider_abc import ServiceProviderABC
from cpl_core.type import T, R
class ServiceProvider(ServiceProviderABC): class ServiceProvider(ServiceProviderABC):
@@ -20,25 +19,21 @@ class ServiceProvider(ServiceProviderABC):
Parameter Parameter
--------- ---------
service_descriptors: list[:class:`cpl_core.dependency_injection.service_descriptor.ServiceDescriptor`] service_descriptors: list[:class:`cpl.dependency.service_descriptor.ServiceDescriptor`]
Descriptor of the service Descriptor of the service
config: :class:`cpl_core.configuration.configuration_abc.ConfigurationABC` config: :class:`cpl.core.configuration.configuration_abc.ConfigurationABC`
CPL Configuration CPL Configuration
db_context: Optional[:class:`cpl_core.database.context.database_context_abc.DatabaseContextABC`] db_context: Optional[:class:`cpl.database.context.database_context_abc.DatabaseContextABC`]
Database representation Database representation
""" """
def __init__( def __init__(
self, self,
service_descriptors: list[ServiceDescriptor], service_descriptors: list[ServiceDescriptor],
config: ConfigurationABC,
db_context: Optional[DatabaseContextABC],
): ):
ServiceProviderABC.__init__(self) ServiceProviderABC.__init__(self)
self._service_descriptors: list[ServiceDescriptor] = service_descriptors self._service_descriptors: list[ServiceDescriptor] = service_descriptors
self._configuration: ConfigurationABC = config
self._database_context = db_context
self._scope: Optional[ScopeABC] = None self._scope: Optional[ScopeABC] = None
def _find_service(self, service_type: type) -> Optional[ServiceDescriptor]: def _find_service(self, service_type: type) -> Optional[ServiceDescriptor]:
@@ -48,7 +43,7 @@ class ServiceProvider(ServiceProviderABC):
return None return None
def _get_service(self, parameter: Parameter) -> Optional[object]: def _get_service(self, parameter: Parameter, origin_service_type: type = None) -> Optional[object]:
for descriptor in self._service_descriptors: for descriptor in self._service_descriptors:
if descriptor.service_type == parameter.annotation or issubclass( if descriptor.service_type == parameter.annotation or issubclass(
descriptor.service_type, parameter.annotation descriptor.service_type, parameter.annotation
@@ -56,7 +51,7 @@ class ServiceProvider(ServiceProviderABC):
if descriptor.implementation is not None: if descriptor.implementation is not None:
return descriptor.implementation return descriptor.implementation
implementation = self.build_service(descriptor.service_type) implementation = self._build_service(descriptor.service_type, origin_service_type=origin_service_type)
if descriptor.lifetime == ServiceLifetimeEnum.singleton: if descriptor.lifetime == ServiceLifetimeEnum.singleton:
descriptor.implementation = implementation descriptor.implementation = implementation
@@ -64,7 +59,7 @@ class ServiceProvider(ServiceProviderABC):
# raise Exception(f'Service {parameter.annotation} not found') # raise Exception(f'Service {parameter.annotation} not found')
def _get_services(self, t: type, *args, **kwargs) -> list[Optional[object]]: def _get_services(self, t: type, *args, service_type: type = None, **kwargs) -> list[Optional[object]]:
implementations = [] implementations = []
for descriptor in self._service_descriptors: for descriptor in self._service_descriptors:
if descriptor.service_type == t or issubclass(descriptor.service_type, t): if descriptor.service_type == t or issubclass(descriptor.service_type, t):
@@ -72,7 +67,7 @@ class ServiceProvider(ServiceProviderABC):
implementations.append(descriptor.implementation) implementations.append(descriptor.implementation)
continue continue
implementation = self.build_service(descriptor.service_type, *args, **kwargs) implementation = self._build_service(descriptor.service_type, *args, service_type, **kwargs)
if descriptor.lifetime == ServiceLifetimeEnum.singleton: if descriptor.lifetime == ServiceLifetimeEnum.singleton:
descriptor.implementation = implementation descriptor.implementation = implementation
@@ -80,35 +75,38 @@ class ServiceProvider(ServiceProviderABC):
return implementations return implementations
def build_by_signature(self, sig: Signature) -> list[R]: def _build_by_signature(self, sig: Signature, origin_service_type: type) -> list[R]:
params = [] params = []
for param in sig.parameters.items(): for param in sig.parameters.items():
parameter = param[1] parameter = param[1]
if parameter.name != "self" and parameter.annotation != Parameter.empty: if parameter.name != "self" and parameter.annotation != Parameter.empty:
if typing.get_origin(parameter.annotation) == list: if typing.get_origin(parameter.annotation) == list:
params.append(self._get_services(typing.get_args(parameter.annotation)[0])) params.append(self._get_services(typing.get_args(parameter.annotation)[0], origin_service_type))
elif parameter.annotation == Source:
params.append(origin_service_type.__name__)
elif issubclass(parameter.annotation, ServiceProviderABC): elif issubclass(parameter.annotation, ServiceProviderABC):
params.append(self) params.append(self)
elif issubclass(parameter.annotation, ApplicationEnvironmentABC): elif issubclass(parameter.annotation, Environment):
params.append(self._configuration.environment) params.append(Environment)
elif issubclass(parameter.annotation, DatabaseContextABC):
params.append(self._database_context)
elif issubclass(parameter.annotation, ConfigurationModelABC): elif issubclass(parameter.annotation, ConfigurationModelABC):
params.append(self._configuration.get_configuration(parameter.annotation)) params.append(Configuration.get(parameter.annotation))
elif issubclass(parameter.annotation, ConfigurationABC): elif issubclass(parameter.annotation, Configuration):
params.append(self._configuration) params.append(Configuration)
else: else:
params.append(self._get_service(parameter)) params.append(self._get_service(parameter, origin_service_type))
return params return params
def build_service(self, service_type: type, *args, **kwargs) -> object: def _build_service(self, service_type: type, *args, origin_service_type: type = None, **kwargs) -> object:
if origin_service_type is None:
origin_service_type = service_type
for descriptor in self._service_descriptors: for descriptor in self._service_descriptors:
if descriptor.service_type == service_type or issubclass(descriptor.service_type, service_type): if descriptor.service_type == service_type or issubclass(descriptor.service_type, service_type):
if descriptor.implementation is not None: if descriptor.implementation is not None:
@@ -119,7 +117,7 @@ class ServiceProvider(ServiceProviderABC):
break break
sig = signature(service_type.__init__) sig = signature(service_type.__init__)
params = self.build_by_signature(sig) params = self._build_by_signature(sig, origin_service_type)
return service_type(*params, *args, **kwargs) return service_type(*params, *args, **kwargs)
@@ -135,7 +133,7 @@ class ServiceProvider(ServiceProviderABC):
else: else:
descriptors.append(copy.deepcopy(descriptor)) descriptors.append(copy.deepcopy(descriptor))
sb = ScopeBuilder(ServiceProvider(descriptors, self._configuration, self._database_context)) sb = ScopeBuilder(ServiceProvider(descriptors, self._database_context))
return sb.build() return sb.build()
def get_service(self, service_type: T, *args, **kwargs) -> Optional[R]: def get_service(self, service_type: T, *args, **kwargs) -> Optional[R]:
@@ -147,7 +145,7 @@ class ServiceProvider(ServiceProviderABC):
if result.implementation is not None: if result.implementation is not None:
return result.implementation return result.implementation
implementation = self.build_service(service_type, *args, **kwargs) implementation = self._build_service(service_type, *args, **kwargs)
if ( if (
result.lifetime == ServiceLifetimeEnum.singleton result.lifetime == ServiceLifetimeEnum.singleton
or result.lifetime == ServiceLifetimeEnum.scoped or result.lifetime == ServiceLifetimeEnum.scoped

View File

@@ -1,14 +1,14 @@
import functools import functools
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from inspect import Signature, signature from inspect import Signature, signature
from typing import Optional, Type from typing import Optional
from cpl_core.dependency_injection.scope_abc import ScopeABC from cpl.dependency.scope_abc import ScopeABC
from cpl_core.type import T, R from cpl.core.typing import T, R
class ServiceProviderABC(ABC): class ServiceProviderABC(ABC):
r"""ABC for the class :class:`cpl_core.dependency_injection.service_provider.ServiceProvider`""" r"""ABC for the class :class:`cpl.dependency.service_provider.ServiceProvider`"""
_provider: Optional["ServiceProviderABC"] = None _provider: Optional["ServiceProviderABC"] = None
@@ -21,11 +21,11 @@ class ServiceProviderABC(ABC):
cls._provider = provider cls._provider = provider
@abstractmethod @abstractmethod
def build_by_signature(self, sig: Signature) -> list[R]: def _build_by_signature(self, sig: Signature, origin_service_type: type) -> list[R]:
pass pass
@abstractmethod @abstractmethod
def build_service(self, service_type: type, *args, **kwargs) -> object: def _build_service(self, service_type: type, *args, **kwargs) -> object:
r"""Creates instance of given type r"""Creates instance of given type
Parameter Parameter
@@ -37,7 +37,6 @@ class ServiceProviderABC(ABC):
------- -------
Object of the given type Object of the given type
""" """
pass
@abstractmethod @abstractmethod
def set_scope(self, scope: ScopeABC): def set_scope(self, scope: ScopeABC):
@@ -45,10 +44,9 @@ class ServiceProviderABC(ABC):
Parameter Parameter
--------- ---------
Object of type :class:`cpl_core.dependency_injection.scope_abc.ScopeABC` Object of type :class:`cpl.dependency.scope_abc.ScopeABC`
Service scope Service scope
""" """
pass
@abstractmethod @abstractmethod
def create_scope(self) -> ScopeABC: def create_scope(self) -> ScopeABC:
@@ -56,9 +54,8 @@ class ServiceProviderABC(ABC):
Returns Returns
------- -------
Object of type :class:`cpl_core.dependency_injection.scope_abc.ScopeABC` Object of type :class:`cpl.dependency.scope_abc.ScopeABC`
""" """
pass
@abstractmethod @abstractmethod
def get_service(self, instance_type: T, *args, **kwargs) -> Optional[R]: def get_service(self, instance_type: T, *args, **kwargs) -> Optional[R]:
@@ -66,14 +63,13 @@ class ServiceProviderABC(ABC):
Parameter Parameter
--------- ---------
instance_type: :class:`cpl_core.type.T` instance_type: :class:`cpl.core.type.T`
The type of the searched instance The type of the searched instance
Returns Returns
------- -------
Object of type Optional[:class:`cpl_core.type.T`] Object of type Optional[:class:`cpl.core.type.T`]
""" """
pass
@abstractmethod @abstractmethod
def get_services(self, service_type: T, *args, **kwargs) -> list[Optional[R]]: def get_services(self, service_type: T, *args, **kwargs) -> list[Optional[R]]:
@@ -81,14 +77,13 @@ class ServiceProviderABC(ABC):
Parameter Parameter
--------- ---------
service_type: :class:`cpl_core.type.T` service_type: :class:`cpl.core.type.T`
The type of the searched instance The type of the searched instance
Returns Returns
------- -------
Object of type list[Optional[:class:`cpl_core.type.T`] Object of type list[Optional[:class:`cpl.core.type.T`]
""" """
pass
@classmethod @classmethod
def inject(cls, f=None): def inject(cls, f=None):
@@ -110,7 +105,7 @@ class ServiceProviderABC(ABC):
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]
return f(*args, *injection, **kwargs) return f(*args, *injection, **kwargs)
return inner return inner

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-dependency"
version = "2024.7.0"
description = "CPL dependency"
readme ="CPL dependency package"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [
{ name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" }
]
keywords = ["cpl", "dependency", "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 @@
cpl-core

View File

@@ -0,0 +1,21 @@
from cpl.dependency import ServiceCollection as _ServiceCollection
from .abc.email_client_abc import EMailClientABC
from .email_client import EMailClient
from .email_client_settings import EMailClientSettings
from .email_client_settings_name_enum import EMailClientSettingsNameEnum
from .email_model import EMail
from .mail_logger import MailLogger
def add_mail(collection: _ServiceCollection):
from cpl.core.console import Console
from cpl.core.log import LoggerABC
try:
collection.add_singleton(EMailClientABC, EMailClient)
collection.add_transient(LoggerABC, MailLogger)
except ImportError as e:
Console.error("cpl-translation is not installed", str(e))
_ServiceCollection.with_module(add_mail, __name__)

View File

@@ -1,10 +1,10 @@
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from cpl_core.mailing.email import EMail from cpl.mail.email_model import EMail
class EMailClientABC(ABC): class EMailClientABC(ABC):
"""ABC of :class:`cpl_core.mailing.email_client_service.EMailClient`""" """ABC of :class:`cpl.mail.email_client_service.EMailClient`"""
@abstractmethod @abstractmethod
def __init__(self): def __init__(self):
@@ -13,14 +13,12 @@ class EMailClientABC(ABC):
@abstractmethod @abstractmethod
def connect(self): def connect(self):
r"""Connects to server""" r"""Connects to server"""
pass
@abstractmethod @abstractmethod
def send_mail(self, email: EMail): def send_mail(self, email: EMail):
r"""Sends email r"""Sends email
Parameter: Parameter:
email: :class:`cpl_core.mailing.email.EMail` email: :class:`cpl.mail.email.EMail`
Object of the E-Mail to send Object of the E-Mail to send
""" """
pass

View File

@@ -0,0 +1,88 @@
import ssl
from smtplib import SMTP
from typing import Optional
from cpl.core.utils.credential_manager import CredentialManager
from cpl.mail.abc.email_client_abc import EMailClientABC
from cpl.mail.email_client_settings import EMailClientSettings
from cpl.mail.email_model import EMail
from cpl.mail.mail_logger import MailLogger
class EMailClient(EMailClientABC):
r"""Service to send emails
Parameter:
environment: :class:`cpl.core.environment.application_environment_abc.ApplicationEnvironmentABC`
Environment of the application
logger: :class:`cpl.core.log.logger_abc.LoggerABC`
The logger to use
mail_settings: :class:`cpl.mail.email_client_settings.EMailClientSettings`
Settings for mailing
"""
def __init__(self, logger: MailLogger, mail_settings: EMailClientSettings):
EMailClientABC.__init__(self)
assert mail_settings is not None, "mail_settings must not be None"
self._mail_settings = mail_settings
self._logger = logger
self._server: Optional[SMTP] = None
self.create()
def create(self):
r"""Creates connection"""
self._logger.trace(f"Started {__name__}.create")
self.connect()
self._logger.trace(f"Stopped {__name__}.create")
def connect(self):
self._logger.trace(f"Started {__name__}.connect")
try:
self._logger.debug(f"Try to connect to {self._mail_settings.host}:{self._mail_settings.port}")
self._server = SMTP(self._mail_settings.host, self._mail_settings.port)
self._logger.info(f"Connected to {self._mail_settings.host}:{self._mail_settings.port}")
self._logger.debug("Try to start tls")
self._server.starttls(context=ssl.create_default_context())
self._logger.info("Started tls")
except Exception as e:
self._logger.error("Cannot connect to mail server", e)
self._logger.trace(f"Stopped {__name__}.connect")
def login(self):
r"""Login to server"""
self._logger.trace(f"Started {__name__}.login")
try:
self._logger.debug(
__name__,
f"Try to login {self._mail_settings.user_name}@{self._mail_settings.host}:{self._mail_settings.port}",
)
self._server.login(
self._mail_settings.user_name, CredentialManager.decrypt(self._mail_settings.credentials)
)
self._logger.info(
__name__,
f"Logged on as {self._mail_settings.user_name} to {self._mail_settings.host}:{self._mail_settings.port}",
)
except Exception as e:
self._logger.error("Cannot login to mail server", e)
self._logger.trace(f"Stopped {__name__}.login")
def send_mail(self, email: EMail):
self._logger.trace(f"Started {__name__}.send_mail")
try:
self.login()
self._logger.debug(f"Try to send email to {email.receiver_list}")
self._server.sendmail(
self._mail_settings.user_name, email.receiver_list, email.get_content(self._mail_settings.user_name)
)
self._logger.info(f"Sent email to {email.receiver_list}")
except Exception as e:
self._logger.error(f"Cannot send mail to {email.receiver_list}", e)
self._logger.trace(f"Stopped {__name__}.send_mail")

View File

@@ -1,4 +1,4 @@
from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC
class EMailClientSettings(ConfigurationModelABC): class EMailClientSettings(ConfigurationModelABC):

View File

@@ -0,0 +1,8 @@
from cpl.core.log.logger import Logger
from cpl.core.typing import Source
class MailLogger(Logger):
def __init__(self, source: Source):
Logger.__init__(self, source, "mail")

View File

@@ -0,0 +1,29 @@
[build-system]
requires = ["setuptools>=70.1.0", "wheel>=0.43.0"]
build-backend = "setuptools.build_meta"
[project]
name = "cpl-mail"
version = "2024.7.0"
description = "CPL mail"
readme = "CPL mail package"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [
{ name = "Sven Heidemann", email = "sven.heidemann@sh-edraft.de" }
]
keywords = ["cpl", "mail", "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 @@
cpl-core

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,5 @@
from .default_lambda import default_lambda
from .ordered_queryable import OrderedQueryable
from .sequence import Sequence
from .ordered_queryable_abc import OrderedQueryableABC
from .queryable_abc import QueryableABC

View File

@@ -1,14 +1,13 @@
from collections.abc import Callable from collections.abc import Callable
from cpl_query.base.queryable_abc import QueryableABC from cpl.query.base.ordered_queryable_abc import OrderedQueryableABC
from cpl_query.base.ordered_queryable_abc import OrderedQueryableABC from cpl.query.exceptions import ArgumentNoneException, ExceptionArgument
from cpl_query.exceptions import ArgumentNoneException, ExceptionArgument
class OrderedQueryable(OrderedQueryableABC): class OrderedQueryable(OrderedQueryableABC):
r"""Implementation of :class: `cpl_query.base.ordered_queryable_abc.OrderedQueryableABC`""" r"""Implementation of :class: `cpl.query.base.ordered_queryable_abc.OrderedQueryableABC`"""
def __init__(self, _t: type, _values: QueryableABC = None, _func: Callable = None): def __init__(self, _t: type, _values: OrderedQueryableABC = None, _func: Callable = None):
OrderedQueryableABC.__init__(self, _t, _values, _func) OrderedQueryableABC.__init__(self, _t, _values, _func)
def then_by(self, _func: Callable) -> OrderedQueryableABC: def then_by(self, _func: Callable) -> OrderedQueryableABC:

View File

@@ -4,7 +4,7 @@ from abc import abstractmethod
from collections.abc import Callable from collections.abc import Callable
from typing import Iterable from typing import Iterable
from cpl_query.base.queryable_abc import QueryableABC from cpl.query.base.queryable_abc import QueryableABC
class OrderedQueryableABC(QueryableABC): class OrderedQueryableABC(QueryableABC):
@@ -23,9 +23,8 @@ class OrderedQueryableABC(QueryableABC):
func: :class:`Callable` func: :class:`Callable`
Returns: Returns:
list of :class:`cpl_query.base.ordered_queryable_abc.OrderedQueryableABC` list of :class:`cpl.query.base.ordered_queryable_abc.OrderedQueryableABC`
""" """
pass
@abstractmethod @abstractmethod
def then_by_descending(self, func: Callable) -> OrderedQueryableABC: def then_by_descending(self, func: Callable) -> OrderedQueryableABC:
@@ -35,6 +34,5 @@ class OrderedQueryableABC(QueryableABC):
func: :class:`Callable` func: :class:`Callable`
Returns: Returns:
list of :class:`cpl_query.base.ordered_queryable_abc.OrderedQueryableABC` list of :class:`cpl.query.base.ordered_queryable_abc.OrderedQueryableABC`
""" """
pass

View File

@@ -1,14 +1,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional, Callable, Union, Iterable, Any, TYPE_CHECKING from typing import Optional, Callable, Union, Iterable, Any
from cpl_query._helper import is_number from cpl.query._helper import is_number
from cpl_query.base import default_lambda from cpl.query.base import default_lambda
from cpl.query.base.sequence import Sequence
if TYPE_CHECKING: from cpl.query.exceptions import (
from cpl_query.base.ordered_queryable_abc import OrderedQueryableABC
from cpl_query.base.sequence import Sequence
from cpl_query.exceptions import (
InvalidTypeException, InvalidTypeException,
ArgumentNoneException, ArgumentNoneException,
ExceptionArgument, ExceptionArgument,
@@ -115,7 +112,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
if _func is None: if _func is None:
_func = default_lambda _func = default_lambda
@@ -317,7 +314,7 @@ class QueryableABC(Sequence):
return _func(min(self, key=_func)) return _func(min(self, key=_func))
def order_by(self, _func: Callable = None) -> OrderedQueryableABC: def order_by(self, _func: Callable = None) -> "OrderedQueryableABC":
r"""Sorts elements by function in ascending order r"""Sorts elements by function in ascending order
Parameter Parameter
@@ -327,12 +324,12 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.ordered_queryable_abc.OrderedQueryableABC` :class: `cpl.query.base.ordered_queryable_abc.OrderedQueryableABC`
""" """
if _func is None: if _func is None:
_func = default_lambda _func = default_lambda
from cpl_query.base.ordered_queryable import OrderedQueryable from cpl.query.base.ordered_queryable import OrderedQueryable
return OrderedQueryable(self.type, sorted(self, key=_func), _func) return OrderedQueryable(self.type, sorted(self, key=_func), _func)
@@ -346,12 +343,12 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.ordered_queryable_abc.OrderedQueryableABC` :class: `cpl.query.base.ordered_queryable_abc.OrderedQueryableABC`
""" """
if _func is None: if _func is None:
_func = default_lambda _func = default_lambda
from cpl_query.base.ordered_queryable import OrderedQueryable from cpl.query.base.ordered_queryable import OrderedQueryable
return OrderedQueryable(self.type, sorted(self, key=_func, reverse=True), _func) return OrderedQueryable(self.type, sorted(self, key=_func, reverse=True), _func)
@@ -360,7 +357,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
return type(self)(self._type, reversed(self._values)) return type(self)(self._type, reversed(self._values))
@@ -369,7 +366,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
if _func is None: if _func is None:
_func = default_lambda _func = default_lambda
@@ -384,7 +381,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
# The line below is pain. I don't understand anything of it... # The line below is pain. I don't understand anything of it...
# written on 09.11.2022 by Sven Heidemann # written on 09.11.2022 by Sven Heidemann
@@ -433,7 +430,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
if _index is None: if _index is None:
raise ArgumentNoneException(ExceptionArgument.index) raise ArgumentNoneException(ExceptionArgument.index)
@@ -450,7 +447,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
if _index is None: if _index is None:
raise ArgumentNoneException(ExceptionArgument.index) raise ArgumentNoneException(ExceptionArgument.index)
@@ -493,7 +490,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
groups = [] groups = []
group = [] group = []
@@ -525,7 +522,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
if _index is None: if _index is None:
raise ArgumentNoneException(ExceptionArgument.index) raise ArgumentNoneException(ExceptionArgument.index)
@@ -542,7 +539,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
index = self.count() - _index index = self.count() - _index
@@ -561,7 +558,7 @@ class QueryableABC(Sequence):
Returns Returns
------- -------
:class: `cpl_query.base.queryable_abc.QueryableABC` :class: `cpl.query.base.queryable_abc.QueryableABC`
""" """
if _func is None: if _func is None:
raise ArgumentNoneException(ExceptionArgument.func) raise ArgumentNoneException(ExceptionArgument.func)

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