From 0019f868ad8dbed10fbb3c0252f4aee228f0229e Mon Sep 17 00:00:00 2001 From: Sven Heidemann Date: Thu, 13 Oct 2022 19:57:56 +0200 Subject: [PATCH] Added flask support #70 --- cpl-workspace.json | 4 +- src/bot/application.py | 19 ++++++- src/bot/config/appsettings.edrafts-lapi.json | 6 +++ src/bot/config/feature-flags.json | 1 + src/bot/module_list.py | 2 + src/bot/startup.py | 4 ++ src/bot_api/__init__.py | 1 + src/bot_api/api.py | 52 +++++++++++++++++++ src/bot_api/api_module.py | 26 ++++++++++ src/bot_api/api_thread.py | 27 ++++++++++ src/bot_api/bot-api.json | 48 +++++++++++++++++ src/bot_api/controller/__init__.py | 0 src/bot_api/controller/api_controller.py | 22 ++++++++ src/bot_api/controller/api_route.py | 0 src/bot_api/logging/__init__.py | 0 src/bot_api/logging/api_logger.py | 11 ++++ src/bot_api/route/__init__.py | 0 src/bot_api/route/route.py | 11 ++++ .../configuration/feature_flags_enum.py | 2 + .../configuration/feature_flags_settings.py | 2 + 20 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 src/bot_api/__init__.py create mode 100644 src/bot_api/api.py create mode 100644 src/bot_api/api_module.py create mode 100644 src/bot_api/api_thread.py create mode 100644 src/bot_api/bot-api.json create mode 100644 src/bot_api/controller/__init__.py create mode 100644 src/bot_api/controller/api_controller.py create mode 100644 src/bot_api/controller/api_route.py create mode 100644 src/bot_api/logging/__init__.py create mode 100644 src/bot_api/logging/api_logger.py create mode 100644 src/bot_api/route/__init__.py create mode 100644 src/bot_api/route/route.py diff --git a/cpl-workspace.json b/cpl-workspace.json index 9841dd57..82fc9abe 100644 --- a/cpl-workspace.json +++ b/cpl-workspace.json @@ -11,13 +11,13 @@ "boot-log": "src/modules/boot_log/boot-log.json", "database": "src/modules/database/database.json", "moderator": "src/modules/moderator/moderator.json", - "permission": "src/modules/permission/permission.json" + "permission": "src/modules/permission/permission.json", + "bot-api": "src/bot_api/bot-api.json" }, "Scripts": { "prod": "export KDB_ENVIRONMENT=production; export KDB_NAME=KDB-Prod; cpl start;", "stage": "export KDB_ENVIRONMENT=staging; export KDB_NAME=KDB-Stage; cpl start;", "dev": "export KDB_ENVIRONMENT=development; export KDB_NAME=KDB-Dev; cpl start;", - "build-docker": "cpl b; docker-compose down; docker build -t kdb .", "compose": "docker-compose up -d", "docker": "cpl build-docker; cpl compose;" diff --git a/src/bot/application.py b/src/bot/application.py index 14ce439d..c557eace 100644 --- a/src/bot/application.py +++ b/src/bot/application.py @@ -7,6 +7,10 @@ from cpl_discord.configuration import DiscordBotSettings from cpl_discord.service import DiscordBotServiceABC, DiscordBotService from cpl_translation import TranslatePipe, TranslationServiceABC, TranslationSettings +from bot_api.api_thread import ApiThread +from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum +from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings + class Application(DiscordBotApplicationABC): @@ -14,6 +18,7 @@ class Application(DiscordBotApplicationABC): DiscordBotApplicationABC.__init__(self, config, services) self._services = services + self._config = config # cpl-core self._logger: LoggerABC = services.get_service(LoggerABC) @@ -24,6 +29,12 @@ class Application(DiscordBotApplicationABC): self._translation: TranslationServiceABC = services.get_service(TranslationServiceABC) self._t: TranslatePipe = services.get_service(TranslatePipe) + self._feature_flags: FeatureFlagsSettings = config.get_configuration(FeatureFlagsSettings) + + # api + if self._feature_flags.get_flag(FeatureFlagsEnum.api_module): + self._api: ApiThread = services.get_service(ApiThread) + self._is_stopping = False async def configure(self): @@ -32,6 +43,12 @@ class Application(DiscordBotApplicationABC): async def main(self): try: self._logger.debug(__name__, f'Starting...') + if self._feature_flags.get_flag(FeatureFlagsEnum.api_module): + self._api.start() + + if self._feature_flags.get_flag(FeatureFlagsEnum.api_only): + return + self._logger.trace(__name__, f'Try to start {DiscordBotService.__name__}') await self._bot.start_async() await self._bot.stop_async() @@ -53,4 +70,4 @@ class Application(DiscordBotApplicationABC): Console.write_line() def is_restart(self): - return True if self._configuration.get_configuration('IS_RESTART') == 'true' else False# + return True if self._configuration.get_configuration('IS_RESTART') == 'true' else False # diff --git a/src/bot/config/appsettings.edrafts-lapi.json b/src/bot/config/appsettings.edrafts-lapi.json index 9a4ccd18..2e9e52a3 100644 --- a/src/bot/config/appsettings.edrafts-lapi.json +++ b/src/bot/config/appsettings.edrafts-lapi.json @@ -12,6 +12,12 @@ "FileLogLevel": "TRACE" }, "BotLoggingSettings": { + "Api": { + "Path": "logs/", + "Filename": "api.log", + "ConsoleLogLevel": "TRACE", + "FileLogLevel": "TRACE" + }, "Command": { "Path": "logs/", "Filename": "commands.log", diff --git a/src/bot/config/feature-flags.json b/src/bot/config/feature-flags.json index b20c9cca..68bf6e00 100644 --- a/src/bot/config/feature-flags.json +++ b/src/bot/config/feature-flags.json @@ -1,5 +1,6 @@ { "FeatureFlags": { + "ApiModule": true, "AdminModule": true, "AutoRoleModule": true, "BaseModule": true, diff --git a/src/bot/module_list.py b/src/bot/module_list.py index 5b8f4e5b..866b7d67 100644 --- a/src/bot/module_list.py +++ b/src/bot/module_list.py @@ -1,5 +1,6 @@ from cpl_query.extension import List +from bot_api.api_module import ApiModule from bot_core.core_extension.core_extension_module import CoreExtensionModule from bot_core.core_module import CoreModule from bot_data.data_module import DataModule @@ -20,6 +21,7 @@ class ModuleList: return List(type, [ CoreModule, # has to be first! DataModule, + ApiModule, AdminModule, AutoRoleModule, BaseModule, diff --git a/src/bot/startup.py b/src/bot/startup.py index f1c123c5..1384fbd9 100644 --- a/src/bot/startup.py +++ b/src/bot/startup.py @@ -9,6 +9,7 @@ from cpl_core.dependency_injection import ServiceProviderABC from cpl_core.environment import ApplicationEnvironment from cpl_core.logging import LoggerABC +from bot_api.logging.api_logger import ApiLogger from bot_core.abc.custom_file_logger_abc import CustomFileLoggerABC from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings @@ -40,6 +41,9 @@ class Startup(StartupABC): services.add_singleton(CustomFileLoggerABC, DatabaseLogger) services.add_singleton(CustomFileLoggerABC, MessageLogger) + if self._feature_flags.get_flag(FeatureFlagsEnum.api_module): + services.add_singleton(CustomFileLoggerABC, ApiLogger) + services.add_translation() services.add_db_context(DBContext, self._config.get_configuration(DatabaseSettings)) diff --git a/src/bot_api/__init__.py b/src/bot_api/__init__.py new file mode 100644 index 00000000..ad5eca30 --- /dev/null +++ b/src/bot_api/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/src/bot_api/api.py b/src/bot_api/api.py new file mode 100644 index 00000000..7a75e2a4 --- /dev/null +++ b/src/bot_api/api.py @@ -0,0 +1,52 @@ +import sys +from functools import partial + +from cpl_core.dependency_injection import ServiceProviderABC +from flask import Flask, request + +from bot_api.logging.api_logger import ApiLogger +from bot_api.route.route import Route + + +class Api(Flask): + + def __init__( + self, + logger: ApiLogger, + services: ServiceProviderABC, + *args, **kwargs + ): + if not args: + kwargs.setdefault('import_name', __name__) + + Flask.__init__(self, *args, **kwargs) + + self._logger = logger + self._services = services + + # register before request + self.before_request_funcs.setdefault(None, []).append(self.before_request) + + def _register_routes(self): + for path, f in Route.registered_routes.items(): + cls = None + qual_name_split = f.__qualname__.split('.') + if len(qual_name_split) > 0: + cls_type = vars(sys.modules[f.__module__])[qual_name_split[0]] + cls = self._services.get_service(cls_type) + + partial_f = partial(f, self if cls is None else cls) + partial_f.__name__ = f.__name__ + self.route(path)(partial_f) + + def before_request(self, *args, **kwargs): + self._logger.debug(__name__, f'Received GET @{request.url}') + headers = str(request.headers).replace("\n", "\n\t") + self._logger.trace(__name__, f'Headers: \n\t{headers}') + self._logger.trace(__name__, f'Body: {request.get_json(force=True, silent=True)}') + + def start(self): + self._logger.info(__name__, f'Starting API') + self._register_routes() + from waitress import serve + serve(self, host="0.0.0.0", port=5000) diff --git a/src/bot_api/api_module.py b/src/bot_api/api_module.py new file mode 100644 index 00000000..91f91794 --- /dev/null +++ b/src/bot_api/api_module.py @@ -0,0 +1,26 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceCollectionABC +from cpl_core.environment import ApplicationEnvironmentABC +from cpl_discord.service.discord_collection_abc import DiscordCollectionABC +from flask import Flask + +from bot_api.api import Api +from bot_api.api_thread import ApiThread +from bot_api.controller.api_controller import ApiController +from bot_core.abc.module_abc import ModuleABC +from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum + + +class ApiModule(ModuleABC): + + def __init__(self, dc: DiscordCollectionABC): + ModuleABC.__init__(self, dc, FeatureFlagsEnum.api_module) + + def configure_configuration(self, config: ConfigurationABC, env: ApplicationEnvironmentABC): + pass + + def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + services.add_singleton(ApiThread) + services.add_singleton(Flask, Api) + + services.add_transient(ApiController) diff --git a/src/bot_api/api_thread.py b/src/bot_api/api_thread.py new file mode 100644 index 00000000..b6a34bde --- /dev/null +++ b/src/bot_api/api_thread.py @@ -0,0 +1,27 @@ +import threading + +from bot_api.api import Api +from bot_api.logging.api_logger import ApiLogger +from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum +from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings + + +class ApiThread(threading.Thread): + + def __init__( + self, + logger: ApiLogger, + api: Api, + feature_flags: FeatureFlagsSettings + ): + threading.Thread.__init__(self, daemon=not feature_flags.get_flag(FeatureFlagsEnum.api_only)) + + self._logger = logger + self._api = api + + def run(self) -> None: + try: + self._logger.trace(__name__, f'Try to start {type(self._api).__name__}') + self._api.start() + except Exception as e: + self._logger.error(__name__, 'Start failed', e) diff --git a/src/bot_api/bot-api.json b/src/bot_api/bot-api.json new file mode 100644 index 00000000..b31234fd --- /dev/null +++ b/src/bot_api/bot-api.json @@ -0,0 +1,48 @@ +{ + "ProjectSettings": { + "Name": "bot-api", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core==2022.10.0.post6", + "Flask==2.2.2", + "Flask-Classful==0.14.2" + ], + "DevDependencies": [ + "cpl-cli>=2022.10.0" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "library", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "bot_api.main", + "EntryPoint": "bot-api", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/src/bot_api/controller/__init__.py b/src/bot_api/controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/bot_api/controller/api_controller.py b/src/bot_api/controller/api_controller.py new file mode 100644 index 00000000..9c714166 --- /dev/null +++ b/src/bot_api/controller/api_controller.py @@ -0,0 +1,22 @@ +from cpl_translation import TranslatePipe + +from bot_api.api import Api +from bot_api.logging.api_logger import ApiLogger +from bot_api.route.route import Route + + +class ApiController: + + def __init__( + self, + logger: ApiLogger, + t: TranslatePipe, + api: Api + ): + self._logger = logger + self._t = t + self._api = api + + @Route.route('/api/hello-world') + def hello_world(self): + return self._t.transform('common.hello_world') diff --git a/src/bot_api/controller/api_route.py b/src/bot_api/controller/api_route.py new file mode 100644 index 00000000..e69de29b diff --git a/src/bot_api/logging/__init__.py b/src/bot_api/logging/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/bot_api/logging/api_logger.py b/src/bot_api/logging/api_logger.py new file mode 100644 index 00000000..1e4f00e8 --- /dev/null +++ b/src/bot_api/logging/api_logger.py @@ -0,0 +1,11 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.environment import ApplicationEnvironmentABC +from cpl_core.time import TimeFormatSettings + +from bot_core.abc.custom_file_logger_abc import CustomFileLoggerABC + + +class ApiLogger(CustomFileLoggerABC): + + def __init__(self, config: ConfigurationABC, time_format: TimeFormatSettings, env: ApplicationEnvironmentABC): + CustomFileLoggerABC.__init__(self, 'Api', config, time_format, env) diff --git a/src/bot_api/route/__init__.py b/src/bot_api/route/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/bot_api/route/route.py b/src/bot_api/route/route.py new file mode 100644 index 00000000..ef0e3994 --- /dev/null +++ b/src/bot_api/route/route.py @@ -0,0 +1,11 @@ +class Route: + registered_routes = {} + + @classmethod + def route(cls, path=None): + # simple decorator for class based views + def inner(fn): + cls.registered_routes[path] = fn + return fn + + return inner diff --git a/src/bot_core/configuration/feature_flags_enum.py b/src/bot_core/configuration/feature_flags_enum.py index dac193b1..b41ec6e5 100644 --- a/src/bot_core/configuration/feature_flags_enum.py +++ b/src/bot_core/configuration/feature_flags_enum.py @@ -4,6 +4,7 @@ from enum import Enum class FeatureFlagsEnum(Enum): # modules + api_module = 'ApiModule' admin_module = 'AdminModule' auto_role_module = 'AutoRoleModule' base_module = 'BaseModule' @@ -15,4 +16,5 @@ class FeatureFlagsEnum(Enum): moderator_module = 'ModeratorModule' permission_module = 'PermissionModule' # features + api_only = 'ApiOnly' presence = 'Presence' diff --git a/src/bot_core/configuration/feature_flags_settings.py b/src/bot_core/configuration/feature_flags_settings.py index 4556e784..1861f9a1 100644 --- a/src/bot_core/configuration/feature_flags_settings.py +++ b/src/bot_core/configuration/feature_flags_settings.py @@ -14,6 +14,7 @@ class FeatureFlagsSettings(ConfigurationModelABC): self._flags = { # modules + FeatureFlagsEnum.api_module.value: False, # 13.10.2022 #70 FeatureFlagsEnum.admin_module.value: False, # 02.10.2022 #48 FeatureFlagsEnum.auto_role_module.value: True, # 03.10.2022 #54 FeatureFlagsEnum.base_module.value: True, # 02.10.2022 #48 @@ -25,6 +26,7 @@ class FeatureFlagsSettings(ConfigurationModelABC): FeatureFlagsEnum.moderator_module.value: False, # 02.10.2022 #48 FeatureFlagsEnum.permission_module.value: True, # 02.10.2022 #48 # features + FeatureFlagsEnum.api_only.value: False, # 13.10.2022 #70 FeatureFlagsEnum.presence.value: True, # 03.10.2022 #56 }