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.
- {chutils-2.2.0 → chutils-2.3.0}/PKG-INFO +50 -11
- {chutils-2.2.0 → chutils-2.3.0}/README.md +48 -9
- {chutils-2.2.0 → chutils-2.3.0}/pyproject.toml +6 -2
- {chutils-2.2.0 → chutils-2.3.0}/src/chutils/config.py +39 -1
- {chutils-2.2.0 → chutils-2.3.0}/src/chutils/logger.py +173 -119
- {chutils-2.2.0 → chutils-2.3.0}/LICENSE +0 -0
- {chutils-2.2.0 → chutils-2.3.0}/src/chutils/__init__.py +0 -0
- {chutils-2.2.0 → chutils-2.3.0}/src/chutils/decorators.py +0 -0
- {chutils-2.2.0 → chutils-2.3.0}/src/chutils/secret_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chutils
|
|
3
|
-
Version: 2.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
280
|
-
max_bytes: int =
|
|
281
|
-
compress: bool =
|
|
282
|
-
backup_count: int =
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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: Тип
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
324
|
+
Настроенный экземпляр ChutilsLogger.
|
|
314
325
|
"""
|
|
315
|
-
global
|
|
316
|
-
logging.
|
|
317
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
return _logger_instance
|
|
330
|
+
# --- Определение словаря настроек для данного логгера ---
|
|
331
|
+
# По умолчанию используем секцию [Logging]
|
|
332
|
+
default_settings = cfg.get('Logging', {})
|
|
341
333
|
|
|
342
|
-
#
|
|
343
|
-
|
|
344
|
-
|
|
334
|
+
# Если указана специфичная секция, ее настройки переопределяют дефолтные
|
|
335
|
+
specific_settings = {}
|
|
336
|
+
if config_section_name:
|
|
337
|
+
specific_settings = cfg.get(config_section_name, {})
|
|
345
338
|
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
when
|
|
407
|
-
interval
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|