From f491c65455e127c55e698ab60857261c3237f127 Mon Sep 17 00:00:00 2001 From: zosimovaa Date: Fri, 31 Oct 2025 18:48:52 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D1=83=20=D0=B8?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B4=D0=B8=D0=BB,=20=D0=B2=D1=81?= =?UTF-8?q?=D0=B5=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + pyproject.toml | 2 +- src/__init__.py | 1 - src/basic_application/__init__.py | 2 + .../config_manager.py | 80 +++++--------- src/basic_application/log_manager.py | 40 +++++++ src/config.yaml | 6 +- src/config_test.yaml | 100 ------------------ src/test.py | 35 ++++++ 9 files changed, 106 insertions(+), 161 deletions(-) delete mode 100644 src/__init__.py create mode 100644 src/basic_application/__init__.py rename src/{config_manager => basic_application}/config_manager.py (64%) create mode 100644 src/basic_application/log_manager.py delete mode 100644 src/config_test.yaml create mode 100644 src/test.py diff --git a/.gitignore b/.gitignore index ea0ab97..f099a05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__ venv/ .vscode/ +log*.log diff --git a/pyproject.toml b/pyproject.toml index d05b9cc..8c8fcf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "config_manager" -version = "1.0.4" +version = "1.1.0" description = "Config manager for building applications" authors = [ { name = "Aleksei Zosimov", email = "lesha.spb@gmail.com" } diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index 4a1ad57..0000000 --- a/src/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from config_manager.config_manager import ConfigManager diff --git a/src/basic_application/__init__.py b/src/basic_application/__init__.py new file mode 100644 index 0000000..3d56751 --- /dev/null +++ b/src/basic_application/__init__.py @@ -0,0 +1,2 @@ +from .config_manager import ConfigManager +from .log_manager import LogManager \ No newline at end of file diff --git a/src/config_manager/config_manager.py b/src/basic_application/config_manager.py similarity index 64% rename from src/config_manager/config_manager.py rename to src/basic_application/config_manager.py index 9749d12..0738b94 100644 --- a/src/config_manager/config_manager.py +++ b/src/basic_application/config_manager.py @@ -1,20 +1,18 @@ +import logging +import logging.config import asyncio import json import yaml -import logging -import logging.config -from typing import Any, Optional import os +from typing import Any, Optional - -logger = logging.getLogger(__name__) - +from .log_manager import LogManager class ConfigManager: DEFAULT_UPDATE_INTERVAL = 5.0 DEFAULT_WORK_INTERVAL = 2.0 - def __init__(self, path: str): + def __init__(self, path: str, log_manager: Optional[LogManager] = None): self.path = path self.config: Any = None self._last_hash = None @@ -23,6 +21,9 @@ class ConfigManager: self._halt = asyncio.Event() self._task: Optional[asyncio.Task] = None self._loop: Optional[asyncio.AbstractEventLoop] = None + + self._log_manager = log_manager or LogManager() + self.logger = logging.getLogger(__name__) def _read_file_sync(self) -> str: with open(self.path, "r", encoding="utf-8") as f: @@ -31,9 +32,9 @@ class ConfigManager: async def _read_file_async(self) -> str: return await asyncio.to_thread(self._read_file_sync) - def _parse_config(self, str) -> Any: - ext = os.path.splitext(self.path)[1].lower() - if ext in (".yaml", ".yml"): + def _parse_config(self, data) -> Any: + extension = os.path.splitext(self.path)[1].lower() + if extension in (".yaml", ".yml"): return yaml.safe_load(data) else: return json.loads(data) @@ -46,25 +47,16 @@ class ConfigManager: if isinstance(upd, (int, float)) and upd > 0: self.update_interval = float(upd) - logger.info(f"Update interval set to {self.update_interval} seconds") + self.logger.info(f"Update interval set to {self.update_interval} seconds") else: self.update_interval = self.DEFAULT_UPDATE_INTERVAL if isinstance(wrk, (int, float)) and wrk > 0: self.work_interval = float(wrk) - logger.info(f"Work interval set to {self.work_interval} seconds") + self.logger.info(f"Work interval set to {self.work_interval} seconds") else: self.work_interval = self.DEFAULT_WORK_INTERVAL - def _apply_logging_config(self, config: dict) -> None: - try: - logging_config = config.get("logging") - if logging_config: - logging.config.dictConfig(logging_config) - logger.info("Logging configuration applied") - except Exception as e: - logger.error(f"Error applying logging config: {e}") - async def _update_config(self) -> None: try: data = await self._read_file_async() @@ -74,12 +66,11 @@ class ConfigManager: self.config = new_config self._last_hash = current_hash - self._apply_logging_config(new_config) + self._log_manager.apply_config(new_config) self._update_intervals_from_config() - logger.info("Config updated: %s", self.config) except Exception as e: - logger.error(f"Error reading/parsing config file: {e}") + self.logger.error(f"Error reading/parsing config file: {e}") def execute(self) -> None: """ @@ -102,68 +93,45 @@ class ConfigManager: async def _run(self) -> None: """Внутренняя корутина, запускающая все циклы""" self._halt.clear() - logger.info("ConfigManager started") + self.logger.info("ConfigManager started") try: await asyncio.gather( self._worker_loop(), self._periodic_update_loop() ) except asyncio.CancelledError: - logger.info("ConfigManager tasks cancelled") + self.logger.info("ConfigManager tasks cancelled") finally: - logger.info("ConfigManager stopped") + self.logger.info("ConfigManager stopped") def start(self) -> None: """Запускает менеджер конфигурации в текущем event loop""" if self._task is not None and not self._task.done(): - logger.warning("ConfigManager is already running") + self.logger.warning("ConfigManager is already running") return try: self._loop = asyncio.get_running_loop() except RuntimeError: - logger.error("start() must be called from within an async context") + self.logger.error("start() must be called from within an async context") raise self._task = self._loop.create_task(self._run()) - logger.info("ConfigManager task created") + self.logger.info("ConfigManager task created") async def stop(self) -> None: """Останавливает менеджер конфигурации и ожидает завершения""" if self._task is None: - logger.warning("ConfigManager is not running") + self.logger.warning("ConfigManager is not running") return - logger.info("ConfigManager stopping...") + self.logger.info("ConfigManager stopping...") self._halt.set() - # Ждём корректного завершения задачи try: await self._task except asyncio.CancelledError: pass self._task = None - logger.info("ConfigManager stopped successfully") - - -# Пример наследования и переопределения execute -class MyApp(ConfigManager): - def execute(self) -> None: - logger.info("Executing blocking work with config: %s", self.config) - - -async def main(): - app = MyApp("config.yaml") - app.start() - await asyncio.sleep(20) - await app.stop() - logger.info("Work finished.") - - -if __name__ == "__main__": - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" - ) - asyncio.run(main()) + self.logger.info("ConfigManager stopped successfully") diff --git a/src/basic_application/log_manager.py b/src/basic_application/log_manager.py new file mode 100644 index 0000000..630041c --- /dev/null +++ b/src/basic_application/log_manager.py @@ -0,0 +1,40 @@ +import logging +from typing import Optional + + +class LogManager: + """ + Управляет конфигурацией логирования приложения. + Применяет конфигурацию из словаря с обработкой ошибок. + """ + + def __init__(self): + self.logger = logging.getLogger(__name__) + self._last_valid_config: Optional[dict] = None + + def apply_config(self, config: dict) -> None: + """ + Применяет конфигурацию логирования из словаря. + При ошибке восстанавливает последний валидный конфиг. + + Args: + config: Словарь с настройками логирования (из файла конфига) + """ + logging_config = config.get("log") + if not logging_config: + return + + try: + logging.config.dictConfig(logging_config) + self._last_valid_config = logging_config + self.logger.info("Logging configuration applied") + except Exception as e: + self.logger.error(f"Error applying logging config: {e}") + + # Если был предыдущий валидный конфиг, восстанавливаем его + if self._last_valid_config: + try: + logging.config.dictConfig(self._last_valid_config) + self.logger.info("Previous logging configuration restored") + except Exception as restore_error: + self.logger.error(f"Error restoring previous config: {restore_error}") diff --git a/src/config.yaml b/src/config.yaml index 1cb8ce5..ee9ff58 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -8,7 +8,7 @@ log: formatters: standard: - format: '%(asctime)s %(name)30s [%(levelname)8s]: %(message)s' + format: '%(asctime)s %(module)15s [%(levelname)8s]: %(message)s' telegram: format: '%(message)s' @@ -40,12 +40,12 @@ log: loggers: '': handlers: [console, file] - level: ERROR + level: INFO propagate: False __main__: handlers: [console, file] - level: WARNING + level: DEBUG propagate: False config_manager: diff --git a/src/config_test.yaml b/src/config_test.yaml deleted file mode 100644 index a47060e..0000000 --- a/src/config_test.yaml +++ /dev/null @@ -1,100 +0,0 @@ -# === Раздел с общими конфигурационными параметрами === -runtime: - symbols: ["BTC_USDT", "ETH_USDT", "USDD_USDT", "TRX_USDT", "BTT_USDT", "NFT_USDT", "XRP_USDT", - "ETH_BTC", "XRP_BTC", "TRX_BTC", "LTC_BTC", "EOS_BTC", "XMR_BTC", "DOGE_BTC", - "NFT_TRX", "ETH_TRX", "JST_TRX", "XRP_TRX", - "ETHBULL_USDT", "BULL_USDT", "BEAR_USDT", "ADABULL_USDT"] - - updateTimeout: 45 - errorTimeout: 10 - -orderbook: - levels: [ 0.0, 0.2, 0.4, 0.6, 0.8, - 1.0, 1.2, 1.4, 1.6, 1.8, - 2.0, 2.2, 2.4, 2.6, 2.8, - 3.0, 3.3, 3.6, 3.9, - 4.2, 4.5, 4.8, - 5.1, 5.4, 5.7, 100 ] - -trades: - depth: 300 - -# === Database params === -db: - #host: 185.117.118.107 - host: 92.53.127.143 - port: 59000 - database: rt5_dev - - -# === Логирование === -log: - version: 1 - disable_existing_loggers: False - - formatters: - standard: - format: '%(asctime)s %(name)30s [%(levelname)8s]: %(message)s' - telegram: - format: '%(message)s' - - handlers: - console: - level: DEBUG - formatter: standard - class: logging.StreamHandler - stream: ext://sys.stdout # Default is stderr - - file: - level: DEBUG - formatter: standard - class: logging.handlers.RotatingFileHandler - filename: logs/log.log - mode: a - maxBytes: 500000 - backupCount: 15 - - telegram: - level: CRITICAL - formatter: telegram - class: logging_telegram_handler.TelegramHandler - chat_id: 211945135 - alias: "PDC" - - -# -- Логгеры -- - loggers: - '': - handlers: [console, file] - level: ERROR - propagate: False - - __main__: - handlers: [console, file, telegram] - level: WARNING - propagate: False - - basic_application: - handlers: [console, file, telegram] - level: INFO - - config_manager: - level: INFO - - log_manager: - level: INFO - - poloniex.public: - level: ERROR - - controllers.abstract: - level: ERROR - - controllers.trades: - level: ERROR - - controllers.orderbook: - level: ERROR - - clickhouse_connector.clickhouse_connector: - level: ERROR \ No newline at end of file diff --git a/src/test.py b/src/test.py new file mode 100644 index 0000000..bc21944 --- /dev/null +++ b/src/test.py @@ -0,0 +1,35 @@ +from basic_application import ConfigManager +import logging +import asyncio +from typing import Optional +import os +os.chdir(os.path.dirname(__file__)) + +logger = logging.getLogger() + + +class MyApp(ConfigManager): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.iter = 0 + + + def execute(self) -> None: + logger.info(f"current iteration {self.iter}") + self.iter += 1 + + +async def main(): + app = MyApp("config.yaml") + app.start() + logger.info("App started") + await asyncio.sleep(20) + await app.stop() + +if __name__ == "__main__": + asyncio.run(main()) + + + + + \ No newline at end of file