chutils 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chutils
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Programming Language :: Python :: 3.14
18
18
  Requires-Dist: dotenv (>=0.9.9,<0.10.0)
19
- Requires-Dist: keyring (>=25.6.0,<26.0.0)
19
+ Requires-Dist: keyring (>=25.7.0,<26.0.0)
20
20
  Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
21
21
  Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
22
22
  Description-Content-Type: text/markdown
@@ -49,7 +49,8 @@ Description-Content-Type: text/markdown
49
49
  ## Ключевые возможности
50
50
 
51
51
  - **✨ Ноль конфигурации:** Библиотека **автоматически** находит корень вашего проекта и файл `config.yml` или
52
- `config.ini`. Если файл не найден, используются безопасные настройки по умолчанию (например, логирование только в консоль).
52
+ `config.ini`. Если файл не найден, используются безопасные настройки по умолчанию (например, логирование только в
53
+ консоль).
53
54
  - **⚙️ Гибкая конфигурация:** Поддержка `YAML` и `INI` форматов. Простые функции для получения типизированных данных.
54
55
  - **✍️ Продвинутый логгер:** Функция `setup_logger()` "из коробки" настраивает логирование в консоль и в ротируемые
55
56
  файлы. Возвращает кастомный логгер с дополнительными уровнями отладки (`devdebug`, `mediumdebug`).
@@ -86,7 +87,8 @@ pip install -e .
86
87
 
87
88
  ### 1. Работа с конфигурацией
88
89
 
89
- 1. (Опционально) Создайте файл `config.yml` в корне вашего проекта. Если этого не сделать, библиотека будет использовать настройки по умолчанию:
90
+ 1. (Опционально) Создайте файл `config.yml` в корне вашего проекта. Если этого не сделать, библиотека будет использовать
91
+ настройки по умолчанию:
90
92
 
91
93
  ```yaml
92
94
  # config.yml
@@ -115,8 +117,9 @@ pip install -e .
115
117
  Вы можете создать локальный файл конфигурации (например, `config.local.yml` или `config.local.ini`) рядом с основным
116
118
  файлом (`config.yml` или `config.ini`). Значения из локального файла будут **переопределять** соответствующие
117
119
  значения из основного файла. Это удобно для:
118
- - Хранения чувствительных данных, которые не должны попадать в систему контроля версий (добавьте `config.local.yml` в `.gitignore`).
119
- - Переопределения настроек для локальной разработки без изменения основного файла.
120
+ - Хранения чувствительных данных, которые не должны попадать в систему контроля версий (добавьте `config.local.yml`
121
+ в `.gitignore`).
122
+ - Переопределения настроек для локальной разработки без изменения основного файла.
120
123
 
121
124
  Пример:
122
125
  Если `config.yml` содержит:
@@ -188,7 +191,8 @@ pip install -e .
188
191
 
189
192
  #### Создание нескольких логгеров
190
193
 
191
- Вы можете создавать разные логгеры для разных частей вашего приложения, передавая уникальное имя в `setup_logger`. Это
194
+ Вы можете создавать разные логгеры для разных частей вашего приложения, передавая уникальное имя в `setup_logger`.
195
+ Это
192
196
  помогает фильтровать и разделять логи.
193
197
 
194
198
  ```python
@@ -206,12 +210,46 @@ pip install -e .
206
210
  В лог-файлах вы увидите сообщения от соответствующих логгеров.
207
211
  Более подробный пример можно найти в [`/examples/05_different_log_levels.py`](./examples/05_different_log_levels.py).
208
212
 
213
+ #### Конфигурация нескольких логгеров через файл
214
+
215
+ Вы можете централизованно управлять настройками разных логгеров, используя параметр `config_section_name`.
216
+
217
+ 1. **Добавьте секции в `config.yml`**:
218
+ Секция `[Logging]` используется для настроек по умолчанию. Остальные секции можно использовать для специфичных
219
+ логгеров.
220
+ ```yaml
221
+ # config.yml
222
+ Logging:
223
+ log_level: INFO
224
+ rotation_type: time
225
+ compress: true
226
+
227
+ AuditLogger:
228
+ log_level: DEBUG
229
+ log_file_name: "audit.log"
230
+ ```
231
+
232
+ 2. **Используйте `config_section_name` в коде**:
233
+ ```python
234
+ # main.py
235
+ from chutils import setup_logger
236
+
237
+ # Этот логгер возьмет настройки из секции [Logging]
238
+ main_logger = setup_logger("main")
239
+ main_logger.info("Сообщение от основного логгера.")
240
+
241
+ # А этот логгер - из секции [AuditLogger], которая переопределит настройки из [Logging]
242
+ audit_logger = setup_logger("audit", config_section_name="AuditLogger")
243
+ audit_logger.debug("Детальное сообщение для аудита.")
244
+ ```
245
+
209
246
  ### 3. Управление секретами
210
247
 
211
248
  `SecretManager` ищет секреты в следующем порядке:
212
- 1. **Системное хранилище (`keyring`)**: Наиболее безопасный способ.
213
- 2. **Файл `.env`**: Если секрет не найден в `keyring`, менеджер будет искать его в файле `.env` в корне вашего проекта.
214
- 3. **Переменные окружения**: Если секрета нет и там, будет произведен поиск в переменных окружения ОС.
249
+
250
+ 1. **Системное хранилище (`keyring`)**: Наиболее безопасный способ.
251
+ 2. **Файл `.env`**: Если секрет не найден в `keyring`, менеджер будет искать его в файле `.env` в корне вашего проекта.
252
+ 3. **Переменные окружения**: Если секрета нет и там, будет произведен поиск в переменных окружения ОС.
215
253
 
216
254
  #### Способ 1: Keyring (рекомендуемый)
217
255
 
@@ -345,7 +383,8 @@ pip install -e .
345
383
 
346
384
  - `setup_logger(name='app_logger', log_level_str='')`: Настраивает и возвращает экземпляр `ChutilsLogger`.
347
385
  - `logger.mediumdebug("message")`: Логирование с уровнем 15. Промежуточный уровень между `DEBUG` и `INFO`.
348
- - `logger.devdebug("message")`: Логирование с уровнем 9. Самый подробный уровень для глубокой отладки (например, для вывода дампов переменных).
386
+ - `logger.devdebug("message")`: Логирование с уровнем 9. Самый подробный уровень для глубокой отладки (например, для
387
+ вывода дампов переменных).
349
388
 
350
389
  ### Управление секретами (`chutils.secret_manager`)
351
390
 
@@ -26,7 +26,8 @@
26
26
  ## Ключевые возможности
27
27
 
28
28
  - **✨ Ноль конфигурации:** Библиотека **автоматически** находит корень вашего проекта и файл `config.yml` или
29
- `config.ini`. Если файл не найден, используются безопасные настройки по умолчанию (например, логирование только в консоль).
29
+ `config.ini`. Если файл не найден, используются безопасные настройки по умолчанию (например, логирование только в
30
+ консоль).
30
31
  - **⚙️ Гибкая конфигурация:** Поддержка `YAML` и `INI` форматов. Простые функции для получения типизированных данных.
31
32
  - **✍️ Продвинутый логгер:** Функция `setup_logger()` "из коробки" настраивает логирование в консоль и в ротируемые
32
33
  файлы. Возвращает кастомный логгер с дополнительными уровнями отладки (`devdebug`, `mediumdebug`).
@@ -63,7 +64,8 @@ pip install -e .
63
64
 
64
65
  ### 1. Работа с конфигурацией
65
66
 
66
- 1. (Опционально) Создайте файл `config.yml` в корне вашего проекта. Если этого не сделать, библиотека будет использовать настройки по умолчанию:
67
+ 1. (Опционально) Создайте файл `config.yml` в корне вашего проекта. Если этого не сделать, библиотека будет использовать
68
+ настройки по умолчанию:
67
69
 
68
70
  ```yaml
69
71
  # config.yml
@@ -92,8 +94,9 @@ pip install -e .
92
94
  Вы можете создать локальный файл конфигурации (например, `config.local.yml` или `config.local.ini`) рядом с основным
93
95
  файлом (`config.yml` или `config.ini`). Значения из локального файла будут **переопределять** соответствующие
94
96
  значения из основного файла. Это удобно для:
95
- - Хранения чувствительных данных, которые не должны попадать в систему контроля версий (добавьте `config.local.yml` в `.gitignore`).
96
- - Переопределения настроек для локальной разработки без изменения основного файла.
97
+ - Хранения чувствительных данных, которые не должны попадать в систему контроля версий (добавьте `config.local.yml`
98
+ в `.gitignore`).
99
+ - Переопределения настроек для локальной разработки без изменения основного файла.
97
100
 
98
101
  Пример:
99
102
  Если `config.yml` содержит:
@@ -165,7 +168,8 @@ pip install -e .
165
168
 
166
169
  #### Создание нескольких логгеров
167
170
 
168
- Вы можете создавать разные логгеры для разных частей вашего приложения, передавая уникальное имя в `setup_logger`. Это
171
+ Вы можете создавать разные логгеры для разных частей вашего приложения, передавая уникальное имя в `setup_logger`.
172
+ Это
169
173
  помогает фильтровать и разделять логи.
170
174
 
171
175
  ```python
@@ -183,12 +187,46 @@ pip install -e .
183
187
  В лог-файлах вы увидите сообщения от соответствующих логгеров.
184
188
  Более подробный пример можно найти в [`/examples/05_different_log_levels.py`](./examples/05_different_log_levels.py).
185
189
 
190
+ #### Конфигурация нескольких логгеров через файл
191
+
192
+ Вы можете централизованно управлять настройками разных логгеров, используя параметр `config_section_name`.
193
+
194
+ 1. **Добавьте секции в `config.yml`**:
195
+ Секция `[Logging]` используется для настроек по умолчанию. Остальные секции можно использовать для специфичных
196
+ логгеров.
197
+ ```yaml
198
+ # config.yml
199
+ Logging:
200
+ log_level: INFO
201
+ rotation_type: time
202
+ compress: true
203
+
204
+ AuditLogger:
205
+ log_level: DEBUG
206
+ log_file_name: "audit.log"
207
+ ```
208
+
209
+ 2. **Используйте `config_section_name` в коде**:
210
+ ```python
211
+ # main.py
212
+ from chutils import setup_logger
213
+
214
+ # Этот логгер возьмет настройки из секции [Logging]
215
+ main_logger = setup_logger("main")
216
+ main_logger.info("Сообщение от основного логгера.")
217
+
218
+ # А этот логгер - из секции [AuditLogger], которая переопределит настройки из [Logging]
219
+ audit_logger = setup_logger("audit", config_section_name="AuditLogger")
220
+ audit_logger.debug("Детальное сообщение для аудита.")
221
+ ```
222
+
186
223
  ### 3. Управление секретами
187
224
 
188
225
  `SecretManager` ищет секреты в следующем порядке:
189
- 1. **Системное хранилище (`keyring`)**: Наиболее безопасный способ.
190
- 2. **Файл `.env`**: Если секрет не найден в `keyring`, менеджер будет искать его в файле `.env` в корне вашего проекта.
191
- 3. **Переменные окружения**: Если секрета нет и там, будет произведен поиск в переменных окружения ОС.
226
+
227
+ 1. **Системное хранилище (`keyring`)**: Наиболее безопасный способ.
228
+ 2. **Файл `.env`**: Если секрет не найден в `keyring`, менеджер будет искать его в файле `.env` в корне вашего проекта.
229
+ 3. **Переменные окружения**: Если секрета нет и там, будет произведен поиск в переменных окружения ОС.
192
230
 
193
231
  #### Способ 1: Keyring (рекомендуемый)
194
232
 
@@ -322,7 +360,8 @@ pip install -e .
322
360
 
323
361
  - `setup_logger(name='app_logger', log_level_str='')`: Настраивает и возвращает экземпляр `ChutilsLogger`.
324
362
  - `logger.mediumdebug("message")`: Логирование с уровнем 15. Промежуточный уровень между `DEBUG` и `INFO`.
325
- - `logger.devdebug("message")`: Логирование с уровнем 9. Самый подробный уровень для глубокой отладки (например, для вывода дампов переменных).
363
+ - `logger.devdebug("message")`: Логирование с уровнем 9. Самый подробный уровень для глубокой отладки (например, для
364
+ вывода дампов переменных).
326
365
 
327
366
  ### Управление секретами (`chutils.secret_manager`)
328
367
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "chutils"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  description = "Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах."
5
5
  authors = ["Chu4hel <sergeiivanov636@gmail.com>"]
6
6
  license = "MIT"
@@ -8,6 +8,7 @@ readme = "README.md"
8
8
  packages = [{ include = "chutils", from = "src" }]
9
9
  exclude = [
10
10
  "tests/",
11
+ "site/",
11
12
  "examples/",
12
13
  "docs/",
13
14
  ".github/",
@@ -20,11 +21,14 @@ exclude = [
20
21
  ".ruff_cache/",
21
22
  "coverage.xml",
22
23
  ".coverage",
24
+ "PUBLISHING.md",
25
+ "project.txt",
26
+ "changelog.txt",
23
27
  ]
24
28
 
25
29
  [tool.poetry.dependencies]
26
30
  python = ">=3.9"
27
- keyring = "^25.6.0"
31
+ keyring = "^25.7.0"
28
32
  pyyaml = "^6.0.3"
29
33
  python-dotenv = "^1.2.1"
30
34
  dotenv = "^0.9.9"
@@ -181,18 +181,56 @@ def _load_yaml(path: str) -> Dict:
181
181
  return {}
182
182
 
183
183
 
184
+ def _nest_ini_dict(flat_dict: Dict[str, Dict[str, Any]]) -> Dict:
185
+ """
186
+ Преобразует плоский словарь INI-секций (с точками в именах секций)
187
+ во вложенную структуру словарей.
188
+ Например: {'Logging.default': {'key': 'value'}} -> {'Logging': {'default': {'key': 'value'}}}
189
+ """
190
+ nested_dict = {}
191
+ for section_key, section_values in flat_dict.items():
192
+ current_level = nested_dict
193
+ parts = section_key.split('.')
194
+ for i, part in enumerate(parts):
195
+ if i == len(parts) - 1: # Последняя часть - это название секции
196
+ current_level[part] = section_values
197
+ else:
198
+ current_level = current_level.setdefault(part, {})
199
+ return nested_dict
200
+
201
+
184
202
  def _load_ini(path: str) -> Dict:
185
203
  """Загружает и парсит INI-файл."""
186
204
  try:
187
205
  with open(path, 'r', encoding='utf-8') as f:
188
206
  parser = configparser.ConfigParser()
189
207
  parser.read_string(f.read())
190
- return {s: dict(parser.items(s)) for s in parser.sections()}
208
+ flat_ini_config = {s: dict(parser.items(s)) for s in parser.sections()}
209
+ # Преобразуем плоскую структуру вложенных секций в иерархическую
210
+ return _nest_ini_dict(flat_ini_config)
191
211
  except (configparser.Error, FileNotFoundError) as e:
192
212
  logger.critical("Ошибка чтения INI файла конфигурации %s: %s", path, e)
193
213
  return {}
194
214
 
195
215
 
216
+ def _nest_ini_dict(flat_dict: Dict[str, Dict[str, Any]]) -> Dict:
217
+ """
218
+ Преобразует плоский словарь INI-секций (с точками в именах секций)
219
+ во вложенную структуру словарей.
220
+ Например: {'Logging.default': {'key': 'value'}} -> {'Logging': {'default': {'key': 'value'}}}
221
+ """
222
+ nested_dict = {}
223
+ for section_key, section_values in flat_dict.items():
224
+ current_level = nested_dict
225
+ parts = section_key.split('.')
226
+ for i, part in enumerate(parts):
227
+ if i == len(parts) - 1: # Последняя часть - это название секции
228
+ current_level[part] = section_values
229
+ else:
230
+ current_level = current_level.setdefault(part, {})
231
+ return nested_dict
232
+
233
+
196
234
  def save_config_value(
197
235
  section: str,
198
236
  key: str,
@@ -6,6 +6,7 @@
6
6
  Директория для логов ('logs') создается автоматически в корне проекта.
7
7
  """
8
8
 
9
+ import datetime
9
10
  import logging
10
11
  import logging.handlers
11
12
  import os
@@ -214,8 +215,6 @@ logging.setLoggerClass(ChutilsLogger)
214
215
 
215
216
  # Кэш для пути к директории логов. Изначально пуст.
216
217
  _LOG_DIR: Optional[str] = None
217
- # Глобальный экземпляр основного логгера приложения
218
- _logger_instance: Optional[ChutilsLogger] = None
219
218
  # Флаг, чтобы сообщение об инициализации выводилось только один раз
220
219
  _initialization_message_shown = False
221
220
 
@@ -273,161 +272,216 @@ def _get_log_dir() -> Optional[str]:
273
272
 
274
273
  def setup_logger(
275
274
  name: str = 'app_logger',
275
+ config_section_name: Optional[str] = None,
276
276
  log_level: Optional[LogLevel] = None,
277
277
  log_file_name: Optional[str] = None,
278
278
  force_reconfigure: bool = False,
279
- rotation_type: str = 'time',
280
- max_bytes: int = 0,
281
- compress: bool = False,
282
- backup_count: int = 3
279
+ rotation_type: Optional[str] = None,
280
+ max_bytes: Optional[int] = None,
281
+ compress: Optional[bool] = None,
282
+ backup_count: Optional[int] = None,
283
+ encoding: Optional[str] = None,
284
+ when: Optional[str] = None,
285
+ interval: Optional[int] = None,
286
+ utc: Optional[bool] = None,
287
+ at_time: Optional[datetime.time] = None,
288
+ **kwargs: Any
283
289
  ) -> ChutilsLogger:
284
290
  """
285
- Настраивает и возвращает логгер с нужным именем.
291
+ Настраивает и возвращает логгер с гибким приоритетом конфигурации.
286
292
 
287
- Функция идемпотентна: она предотвращает повторную настройку уже
288
- существующего логгера. Настройки (уровень, имя файла и т.д.) читаются
289
- из конфигурационного файла. По умолчанию добавляются обработчики для
290
- вывода в консоль и в файл с ежедневной ротацией.
293
+ Приоритет настроек:
294
+ 1. Явные аргументы, переданные в эту функцию.
295
+ 2. Объединенные настройки из `Logging.loggers.{name}` (если есть) поверх `Logging.default` (если есть).
296
+ 3. Для обратной совместимости: если `Logging.default` и `Logging.loggers` отсутствуют,
297
+ используются прямые настройки из `Logging` (как в старом формате).
298
+ 4. Значения по умолчанию, зашитые в коде.
291
299
 
292
300
  Args:
293
- name: Имя логгера. `app_logger` используется для основного логгера
294
- приложения и его экземпляр кэшируется.
295
- log_level: Явное указание уровня логирования. Если не задан,
296
- значение берется из конфигурационного файла, а если и там нет -
297
- используется 'INFO'.
298
- log_file_name: Опциональное имя файла для логирования. Если указано,
299
- логгер будет писать в этот файл. Если не указано, имя файла
300
- берется из конфигурационного файла ('Logging', 'log_file_name').
301
+ name: Имя логгера. `app_logger` используется как стандартное имя.
302
+ config_section_name: Имя специфичной секции в конфиге (например, 'MyAuditLogger').
303
+ Если указана, настройки из этой секции переопределяют настройки из общей секции `[Logging]`.
304
+ Если не указана, используется только общая секция `[Logging]`.
305
+ log_level: Явное указание уровня логирования (строкой или LogLevel).
306
+ Если не задан, значение берется из конфигурационного файла.
307
+ log_file_name: Имя файла для логирования. Если не указано, имя берется
308
+ из конфигурации ('Logging', 'log_file_name').
301
309
  force_reconfigure: Если True, принудительно удаляет все существующие
302
310
  обработчики и настраивает логгер заново.
303
- rotation_type: Тип ротации логов. Может быть 'time' (по умолчанию, ежедневная)
304
- или 'size' (по размеру файла).
305
- max_bytes: Максимальный размер файла лога в байтах перед ротацией,
306
- если `rotation_type` установлен в 'size'. По умолчанию 0 (без лимита).
307
- compress: Если True, ротированные файлы логов будут сжиматься в формат .gz.
308
- По умолчанию False.
309
- backup_count: Количество хранимых ротированных файлов логов.
310
- Старые файлы будут удаляться. По умолчанию 3.
311
+ rotation_type: Тип ротации: 'time' или 'size'.
312
+ max_bytes: Максимальный размер файла для ротации по 'size'.
313
+ compress: Сжимать ли ротированные логи в .gz.
314
+ backup_count: Количество хранимых ротированных файлов.
315
+ encoding: Кодировка файла (по умолчанию 'utf-8').
316
+ when: Для 'time'. Тип интервала ('S', 'M', 'H', 'D', 'midnight', 'W0'-'W6').
317
+ interval: Для 'time'. Длина интервала.
318
+ utc: Для 'time'. Использовать UTC время.
319
+ at_time: Для 'time'. Время ротации (при when='midnight').
320
+
321
+ **kwargs: Дополнительные параметры для FileHandler (например, `delay=True`, `errors='ignore'`, `mode='a'`).
311
322
 
312
323
  Returns:
313
- logging.Logger: Настроенный экземпляр ChutilsLogger.
324
+ Настроенный экземпляр ChutilsLogger.
314
325
  """
315
- global _logger_instance, _initialization_message_shown
316
- logging.debug(
317
- "Вызов setup_logger() для логгера '%s'. log_file_name: %s, force_reconfigure: %s",
318
- name,
319
- log_file_name,
320
- force_reconfigure
321
- )
322
-
323
- # Если логгер с таким именем уже имеет обработчики, значит он настроен.
324
- # Просто возвращаем его, чтобы не дублировать вывод.
325
- existing_logger = logging.getLogger(name)
326
- if existing_logger.hasHandlers() and not force_reconfigure:
327
- logging.debug("Логгер '%s' уже настроен, возвращаем существующий экземпляр.", name)
328
- return existing_logger # type: ignore
329
-
330
- # Если требуется принудительная перенастройка, очищаем старые обработчики
331
- if force_reconfigure:
332
- logging.debug("Принудительная перенастройка для '%s'. Удаление старых обработчиков...", name)
333
- for handler in existing_logger.handlers[:]:
334
- handler.close() # Закрываем файлы, если они были открыты
335
- existing_logger.removeHandler(handler)
326
+ global _initialization_message_shown
327
+ logger = logging.getLogger(name)
328
+ cfg = config.get_config()
336
329
 
337
- # Если запрашивается основной логгер приложения и он уже есть в кэше.
338
- if name == 'app_logger' and _logger_instance:
339
- logging.debug("Возвращаем кэшированный основной логгер.")
340
- return _logger_instance
330
+ # --- Определение словаря настроек для данного логгера ---
331
+ # По умолчанию используем секцию [Logging]
332
+ default_settings = cfg.get('Logging', {})
341
333
 
342
- # Получаем директорию для логов. Это первая точка, где запускается вся магия поиска путей.
343
- log_dir = _get_log_dir()
344
- logging.debug("setup_logger() получил log_dir: %s", log_dir)
334
+ # Если указана специфичная секция, ее настройки переопределяют дефолтные
335
+ specific_settings = {}
336
+ if config_section_name:
337
+ specific_settings = cfg.get(config_section_name, {})
345
338
 
346
- # Загружаем конфигурацию для получения настроек логирования.
347
- cfg = config.get_config()
339
+ final_logger_settings = {**default_settings, **specific_settings}
340
+
341
+ # --- 1. Определение и установка уровня логирования ---
342
+ # Приоритет: аргумент функции > настройки из конфига > 'INFO'
343
+ final_log_level_str: str
344
+ if log_level is not None:
345
+ final_log_level_str = log_level.value if isinstance(log_level, LogLevel) else str(log_level).upper()
346
+ else:
347
+ level_val = final_logger_settings.get('log_level', 'INFO')
348
+ final_log_level_str = str(level_val).upper()
349
+
350
+ try:
351
+ log_level_enum = LogLevel(final_log_level_str)
352
+ level_int = getattr(logging, log_level_enum.value, logging.INFO)
353
+ except ValueError:
354
+ log_level_enum = LogLevel.INFO
355
+ level_int = logging.INFO
348
356
 
349
- # Определяем уровень логирования
350
- if log_level is None:
351
- level_from_config = config.get_config_value('Logging', 'log_level', 'INFO', cfg)
352
- try:
353
- log_level = LogLevel(level_from_config.upper())
354
- except ValueError:
355
- log_level = LogLevel.INFO
356
-
357
- level_int = getattr(logging, log_level.value, logging.INFO)
358
- existing_logger.setLevel(level_int)
359
- logging.debug("Уровень логирования для '%s' установлен на: %s (%s)", name, log_level.value, level_int)
360
-
361
- # Определяем имя файла лога
362
- if log_file_name is None:
363
- log_file_name = config.get_config_value('Logging', 'log_file_name', 'app.log', cfg)
364
- logging.debug("Имя файла лога для '%s' определено как: %s", name, log_file_name)
365
-
366
- # Определяем параметры ротации
367
- rotation_type = config.get_config_value('Logging', 'rotation_type', rotation_type, cfg)
368
- max_bytes = config.get_config_int('Logging', 'max_bytes', max_bytes, cfg)
369
- compress = config.get_config_boolean('Logging', 'compress', compress, cfg)
370
- backup_count = config.get_config_int('Logging', 'log_backup_count', 3, cfg)
371
-
372
- # Создаем и настраиваем новый экземпляр логгера
373
- logger = existing_logger
374
357
  logger.setLevel(level_int)
358
+ logger.propagate = False
359
+ logging.debug("Уровень логирования для '%s' установлен на: %s (%s)", name, log_level_enum.value, level_int)
360
+
361
+ # --- Настройка обработчиков (только при необходимости) ---
362
+ if logger.hasHandlers() and not force_reconfigure:
363
+ logging.debug("Обработчики для логгера '%s' уже настроены. Пропускаем настройку.", name)
364
+ return logger # type: ignore
365
+
366
+ if force_reconfigure:
367
+ logging.debug("Принудительная перенастройка для '%s'. Удаление старых обработчиков...", name)
368
+ for handler in logger.handlers[:]:
369
+ handler.close()
370
+ logger.removeHandler(handler)
371
+
372
+ # --- Определение параметров на основе приоритетов ---
373
+ # Приоритет: аргумент функции > настройки из конфига > жестко заданное значение
374
+ final_log_file_name = log_file_name if log_file_name is not None else final_logger_settings.get('log_file_name',
375
+ 'app.log')
376
+ final_rotation_type = rotation_type if rotation_type is not None else final_logger_settings.get('rotation_type',
377
+ 'time')
378
+
379
+ # Для типизированных значений нужна безопасная обработка
380
+ try:
381
+ max_bytes_from_config = int(final_logger_settings.get('max_bytes', 5 * 1024 * 1024))
382
+ except (ValueError, TypeError):
383
+ max_bytes_from_config = 5 * 1024 * 1024
384
+ final_max_bytes = max_bytes if max_bytes is not None else max_bytes_from_config
385
+
386
+ compress_val = final_logger_settings.get('compress', False)
387
+ if isinstance(compress_val, str):
388
+ compress_from_config = compress_val.lower() in ['true', '1', 't', 'y', 'yes']
389
+ else:
390
+ compress_from_config = bool(compress_val)
391
+ final_compress = compress if compress is not None else compress_from_config
392
+
393
+ try:
394
+ backup_count_from_config = int(final_logger_settings.get('log_backup_count', 3))
395
+ except (ValueError, TypeError):
396
+ backup_count_from_config = 3
397
+ final_backup_count = backup_count if backup_count is not None else backup_count_from_config
398
+
399
+ final_encoding = encoding if encoding is not None else final_logger_settings.get('encoding', 'utf-8')
400
+ final_when = when if when is not None else final_logger_settings.get('when', 'D')
401
+
402
+ try:
403
+ interval_from_config = int(final_logger_settings.get('interval', 1))
404
+ except (ValueError, TypeError):
405
+ interval_from_config = 1
406
+ final_interval = interval if interval is not None else interval_from_config
407
+
408
+ utc_val = final_logger_settings.get('utc', False)
409
+ if isinstance(utc_val, str):
410
+ utc_from_config = utc_val.lower() in ['true', '1', 't', 'y', 'yes']
411
+ else:
412
+ utc_from_config = bool(utc_val)
413
+ final_utc = utc if utc is not None else utc_from_config
414
+
415
+ final_at_time = at_time if at_time is not None else final_logger_settings.get('at_time', None)
416
+
417
+ # --- 4. Настройка обработчиков ---
418
+ log_dir = _get_log_dir()
375
419
  formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
376
420
 
377
- # Обработчик для вывода в консоль (StreamHandler)
378
421
  console_handler = logging.StreamHandler()
379
422
  console_handler.setFormatter(formatter)
380
423
  logger.addHandler(console_handler)
381
424
 
382
- # Обработчик для записи в файл (TimedRotatingFileHandler)
383
- # Добавляем его, только если директория логов была успешно определена.
384
- if log_dir and log_file_name:
385
- # ЕСЛИ ПУТЬ ПЕРЕДАН ЯВНО И ОН АБСОЛЮТНЫЙ, ИСПОЛЬЗУЕМ ЕГО
386
- # Это нужно для нашего отладочного теста, который работает во временной папке
387
- if Path(log_file_name).is_absolute():
388
- log_file_path = Path(log_file_name)
389
- else:
390
- log_file_path = Path(log_dir) / log_file_name
425
+ if log_dir and final_log_file_name:
426
+ log_file_path = Path(final_log_file_name) if Path(
427
+ final_log_file_name).is_absolute() else Path(log_dir) / final_log_file_name
391
428
  logging.debug("Попытка настроить файловый обработчик для %s в %s", name, log_file_path)
392
429
  try:
393
430
  file_handler: Optional[logging.FileHandler] = None
394
- if rotation_type == 'size':
395
- handler_class = CompressingRotatingFileHandler if compress else logging.handlers.RotatingFileHandler
396
- file_handler = handler_class(
397
- log_file_path,
398
- maxBytes=max_bytes,
399
- backupCount=backup_count,
400
- encoding='utf-8'
401
- )
431
+ common_kwargs = {
432
+ 'encoding': final_encoding,
433
+ 'backupCount': final_backup_count,
434
+ }
435
+ common_kwargs.update(kwargs)
436
+
437
+ if final_rotation_type == 'size':
438
+ handler_class = CompressingRotatingFileHandler if final_compress else logging.handlers.RotatingFileHandler
439
+ rotation_kwargs = {'maxBytes': final_max_bytes}
440
+ final_kwargs = {**common_kwargs, **rotation_kwargs}
441
+ file_handler = handler_class(str(log_file_path), **final_kwargs)
402
442
  else: # 'time'
403
- handler_class = CompressingTimedRotatingFileHandler if compress else SafeTimedRotatingFileHandler
404
- file_handler = handler_class(
405
- log_file_path,
406
- when="D",
407
- interval=1,
408
- backupCount=backup_count,
409
- encoding='utf-8'
410
- )
443
+ handler_class = CompressingTimedRotatingFileHandler if final_compress else SafeTimedRotatingFileHandler
444
+
445
+ rotation_kwargs = {
446
+ 'when': final_when,
447
+ 'interval': final_interval,
448
+ 'utc': final_utc,
449
+ }
450
+ # `at_time` может быть строкой из конфига, нужно преобразовать
451
+ if isinstance(final_at_time, str):
452
+ try:
453
+ final_at_time = datetime.time.fromisoformat(final_at_time)
454
+ except (TypeError, ValueError):
455
+ logger.error(
456
+ "Неверный формат времени '%s' для 'at_time' в конфиге. Используется None.", final_at_time)
457
+ final_at_time = None
458
+
459
+ if final_at_time is not None:
460
+ rotation_kwargs['atTime'] = final_at_time
461
+
462
+ final_kwargs = {**common_kwargs, **rotation_kwargs}
463
+
464
+ file_handler = handler_class(str(log_file_path), **final_kwargs)
411
465
 
412
466
  if file_handler:
413
467
  file_handler.setFormatter(formatter)
414
468
  logger.addHandler(file_handler)
415
469
 
416
470
  if not _initialization_message_shown:
471
+ info_msg = "."
472
+ if final_rotation_type == 'time':
473
+ info_msg = f", интервал: {final_interval}{final_when}"
474
+ else:
475
+ info_msg = f", макс. размер: {final_max_bytes}"
417
476
  logger.debug(
418
- "Логирование настроено. Уровень: %s. Файл: %s, ротация: %s, сжатие: %s.",
419
- log_level.value, log_file_path, rotation_type, compress
477
+ "Логирование настроено. Уровень: %s. Файл: %s, ротация: %s, сжатие: %s%s",
478
+ log_level_enum.value, log_file_path, final_rotation_type, final_compress, info_msg
420
479
  )
421
480
  _initialization_message_shown = True
422
481
  except Exception as e:
423
482
  logger.error("Не удалось настроить файловый обработчик логов для %s: %s", log_file_path, e)
424
- else:
425
- if not _initialization_message_shown:
426
- logger.warning("Директория для логов не настроена. Файловое логирование отключено.")
427
- _initialization_message_shown = True
428
-
429
- # Кэшируем основной логгер приложения
430
- if name == 'app_logger':
431
- _logger_instance = logger
483
+ elif not _initialization_message_shown:
484
+ logger.warning("Директория для логов не настроена. Файловое логирование отключено.")
485
+ _initialization_message_shown = True
432
486
 
433
487
  return logger # type: ignore
File without changes
File without changes