chutils 1.0.0__py3-none-any.whl
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/__init__.py +104 -0
- chutils/config.py +267 -0
- chutils/logger.py +186 -0
- chutils-1.0.0.dist-info/LICENSE +21 -0
- chutils-1.0.0.dist-info/METADATA +155 -0
- chutils-1.0.0.dist-info/RECORD +7 -0
- chutils-1.0.0.dist-info/WHEEL +4 -0
chutils/__init__.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Пакет chutils - набор переиспользуемых утилит для Python.
|
|
3
|
+
|
|
4
|
+
Основная цель - упростить рутинные задачи, такие как работа с конфигурацией
|
|
5
|
+
и настройка логирования, с минимальными усилиями со стороны разработчика.
|
|
6
|
+
|
|
7
|
+
Ключевая особенность - автоматическое обнаружение корня проекта. Вам не нужно
|
|
8
|
+
вручную указывать пути к файлу 'config.ini' или папке 'logs'. Пакет сам найдет
|
|
9
|
+
их, ориентируясь на наличие 'config.ini' или 'pyproject.toml' в вашем проекте.
|
|
10
|
+
|
|
11
|
+
Основное использование (в 99% случаев):
|
|
12
|
+
-------------------------------------------
|
|
13
|
+
Вам не нужно ничего инициализировать. Просто импортируйте и используйте:
|
|
14
|
+
|
|
15
|
+
from chutils.config import get_config_value
|
|
16
|
+
from chutils.logger import setup_logger
|
|
17
|
+
|
|
18
|
+
logger = setup_logger()
|
|
19
|
+
db_host = get_config_value("Database", "host", "localhost")
|
|
20
|
+
logger.info(f"Подключение к базе данных на {db_host}")
|
|
21
|
+
|
|
22
|
+
Ручная инициализация (для нестандартных случаев):
|
|
23
|
+
-------------------------------------------------
|
|
24
|
+
Если автоматика не сработала (например, у вас сложная структура проекта),
|
|
25
|
+
вы всегда можете указать путь к корню проекта вручную в самом начале
|
|
26
|
+
работы вашего приложения:
|
|
27
|
+
|
|
28
|
+
import chutils
|
|
29
|
+
chutils.init(base_dir="/path/to/your/project")
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
|
|
35
|
+
# Импортируем модули config и logger, чтобы их внутренние переменные
|
|
36
|
+
# были доступны для функции init.
|
|
37
|
+
from . import config
|
|
38
|
+
from . import logger
|
|
39
|
+
|
|
40
|
+
# --- Импорт публичных функций ---
|
|
41
|
+
# Мы явно импортируем только те функции, которые предназначены для
|
|
42
|
+
# конечного пользователя. Это формирует "чистый" публичный API пакета.
|
|
43
|
+
|
|
44
|
+
from .config import (
|
|
45
|
+
load_config, save_config_value, get_config, get_config_value,
|
|
46
|
+
get_config_int, get_config_float, get_config_boolean, get_config_list,
|
|
47
|
+
get_multiple_config_values, get_config_section
|
|
48
|
+
)
|
|
49
|
+
from .logger import setup_logger
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def init(base_dir: str):
|
|
53
|
+
"""
|
|
54
|
+
Ручная инициализация пакета с указанием базовой директории проекта.
|
|
55
|
+
|
|
56
|
+
Эту функцию нужно вызывать только в том случае, если автоматическое
|
|
57
|
+
определение корня проекта не сработало. Вызывать следует один раз
|
|
58
|
+
в самом начале работы основного скрипта вашего приложения.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
base_dir (str): Абсолютный путь к корневой директории проекта,
|
|
62
|
+
где лежит 'config.ini'.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: Если указанная директория не существует.
|
|
66
|
+
"""
|
|
67
|
+
# Проверяем, что переданный путь является существующей директорией
|
|
68
|
+
if not os.path.isdir(base_dir):
|
|
69
|
+
raise ValueError(f"Указанная директория base_dir не существует или не является директорией: {base_dir}")
|
|
70
|
+
|
|
71
|
+
# Вручную устанавливаем внутренние переменные в модуле config.
|
|
72
|
+
# Это переопределит любые попытки автоматического поиска.
|
|
73
|
+
config._BASE_DIR = base_dir
|
|
74
|
+
config._CONFIG_FILE_PATH = os.path.join(base_dir, "config.ini")
|
|
75
|
+
config._paths_initialized = True
|
|
76
|
+
|
|
77
|
+
# Выводим сообщение, чтобы было понятно, что произошла ручная инициализация.
|
|
78
|
+
print(f"Пакет chutils вручную инициализирован с базовой директорией: {base_dir}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --- Определение публичного API ---
|
|
82
|
+
# `__all__` — это специальный список, который определяет, какие имена будут
|
|
83
|
+
# импортированы, когда пользователь выполнит `from chutils import *`.
|
|
84
|
+
# Это также является явным соглашением о том, что является публичным API пакета.
|
|
85
|
+
|
|
86
|
+
__all__ = [
|
|
87
|
+
# Основная функция ручной инициализации
|
|
88
|
+
'init',
|
|
89
|
+
|
|
90
|
+
# Функции из модуля config
|
|
91
|
+
'load_config',
|
|
92
|
+
'save_config_value',
|
|
93
|
+
'get_config',
|
|
94
|
+
'get_config_value',
|
|
95
|
+
'get_config_int',
|
|
96
|
+
'get_config_float',
|
|
97
|
+
'get_config_boolean',
|
|
98
|
+
'get_config_list',
|
|
99
|
+
'get_multiple_config_values',
|
|
100
|
+
'get_config_section',
|
|
101
|
+
|
|
102
|
+
# Функции из модуля logger
|
|
103
|
+
'setup_logger',
|
|
104
|
+
]
|
chutils/config.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Модуль для работы с конфигурацией.
|
|
3
|
+
|
|
4
|
+
Обеспечивает автоматический поиск файла `config.ini` в корне проекта
|
|
5
|
+
и предоставляет удобные функции для чтения и записи настроек.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import configparser
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional, List, Dict
|
|
14
|
+
|
|
15
|
+
# Настраиваем логгер для этого модуля
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# --- Глобальное состояние для хранения путей ---
|
|
19
|
+
# Эти переменные инициализируются один раз при первом обращении к конфигурации.
|
|
20
|
+
|
|
21
|
+
_BASE_DIR: Optional[str] = None
|
|
22
|
+
_CONFIG_FILE_PATH: Optional[str] = None
|
|
23
|
+
_paths_initialized = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_project_root(start_path: Path, markers: List[str]) -> Optional[Path]:
|
|
27
|
+
"""
|
|
28
|
+
Ищет корень проекта, двигаясь вверх по дереву каталогов от `start_path`.
|
|
29
|
+
|
|
30
|
+
Корень определяется наличием одного из файлов-маркеров (например, 'config.ini').
|
|
31
|
+
"""
|
|
32
|
+
current_path = start_path.resolve()
|
|
33
|
+
# Идем вверх до тех пор, пока не достигнем корня файловой системы
|
|
34
|
+
while current_path != current_path.parent:
|
|
35
|
+
for marker in markers:
|
|
36
|
+
if (current_path / marker).exists():
|
|
37
|
+
logger.debug(f"Найден маркер '{marker}' в директории: {current_path}")
|
|
38
|
+
return current_path
|
|
39
|
+
current_path = current_path.parent
|
|
40
|
+
logger.debug("Корень проекта не найден.")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _initialize_paths():
|
|
45
|
+
"""
|
|
46
|
+
Автоматически находит и устанавливает пути. Вызывается при первом доступе.
|
|
47
|
+
Это "сердце" автоматического обнаружения.
|
|
48
|
+
"""
|
|
49
|
+
global _BASE_DIR, _CONFIG_FILE_PATH, _paths_initialized
|
|
50
|
+
if _paths_initialized:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Ищем корень проекта, начиная от текущей рабочей директории.
|
|
54
|
+
# Приоритетный маркер — 'config.ini', запасной — 'pyproject.toml'.
|
|
55
|
+
project_root = find_project_root(Path.cwd(), markers=['config.ini', 'pyproject.toml'])
|
|
56
|
+
|
|
57
|
+
if project_root:
|
|
58
|
+
_BASE_DIR = str(project_root)
|
|
59
|
+
_CONFIG_FILE_PATH = os.path.join(_BASE_DIR, "config.ini")
|
|
60
|
+
logger.info(f"Корень проекта автоматически определен: {_BASE_DIR}")
|
|
61
|
+
else:
|
|
62
|
+
# Если не нашли, оставляем пути пустыми. Функции ниже будут выбрасывать ошибку.
|
|
63
|
+
logger.warning("Не удалось автоматически найти корень проекта (отсутствуют config.ini или pyproject.toml).")
|
|
64
|
+
|
|
65
|
+
_paths_initialized = True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _get_config_path(cfg_file: Optional[str] = None) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Внутренняя функция-шлюз для получения пути к файлу конфигурации.
|
|
71
|
+
|
|
72
|
+
Если путь не был установлен, запускает автоматический поиск.
|
|
73
|
+
Если путь не передан явно и автоматический поиск не дал результатов,
|
|
74
|
+
выбрасывает исключение с понятным сообщением.
|
|
75
|
+
"""
|
|
76
|
+
# Если путь к файлу передан явно, используем его.
|
|
77
|
+
if cfg_file:
|
|
78
|
+
return cfg_file
|
|
79
|
+
|
|
80
|
+
# Если пути еще не инициализированы, запускаем поиск.
|
|
81
|
+
if not _paths_initialized:
|
|
82
|
+
_initialize_paths()
|
|
83
|
+
|
|
84
|
+
# Если после инициализации путь все еще не определен, это ошибка.
|
|
85
|
+
if _CONFIG_FILE_PATH is None:
|
|
86
|
+
raise FileNotFoundError(
|
|
87
|
+
"Файл конфигурации не найден. Не удалось автоматически определить корень проекта. "
|
|
88
|
+
"Убедитесь, что в корне вашего проекта есть 'config.ini' или 'pyproject.toml', "
|
|
89
|
+
"либо укажите путь к конфигу вручную через chutils.init(base_dir=...)"
|
|
90
|
+
)
|
|
91
|
+
return _CONFIG_FILE_PATH
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_config(cfg_file: Optional[str] = None) -> configparser.ConfigParser:
|
|
95
|
+
"""
|
|
96
|
+
Загружает конфигурацию из .ini файла.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
cfg_file (str, optional): Явный путь к файлу конфигурации.
|
|
100
|
+
Если не указан, будет использован автоматически найденный путь.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
configparser.ConfigParser: Загруженный объект конфигурации.
|
|
104
|
+
"""
|
|
105
|
+
path = _get_config_path(cfg_file)
|
|
106
|
+
if not os.path.exists(path):
|
|
107
|
+
logger.critical(f"Файл конфигурации НЕ НАЙДЕН: {path}")
|
|
108
|
+
return configparser.ConfigParser()
|
|
109
|
+
|
|
110
|
+
config = configparser.ConfigParser()
|
|
111
|
+
try:
|
|
112
|
+
config.read(path, encoding='utf-8')
|
|
113
|
+
logger.info(f"Конфигурация успешно загружена из {path}")
|
|
114
|
+
return config
|
|
115
|
+
except configparser.Error as e:
|
|
116
|
+
logger.critical(f"Ошибка чтения файла конфигурации {path}: {e}")
|
|
117
|
+
return configparser.ConfigParser()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def save_config_value(section: str, key: str, value: str, cfg_file: Optional[str] = None) -> bool:
|
|
121
|
+
"""
|
|
122
|
+
Сохраняет одно значение в конфигурационном файле, пытаясь сохранить комментарии.
|
|
123
|
+
|
|
124
|
+
Изменяет только первую найденную строку с ключом в нужной секции.
|
|
125
|
+
Не добавляет новые секции или ключи, если они не существуют.
|
|
126
|
+
"""
|
|
127
|
+
path = _get_config_path(cfg_file)
|
|
128
|
+
if not os.path.exists(path):
|
|
129
|
+
logger.error(f"Невозможно сохранить значение: файл конфигурации {path} не найден.")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
134
|
+
lines = f.readlines()
|
|
135
|
+
except IOError as e:
|
|
136
|
+
logger.error(f"Ошибка чтения файла {path} для сохранения: {e}")
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
updated = False
|
|
140
|
+
in_target_section = False
|
|
141
|
+
section_found = False
|
|
142
|
+
key_found_in_section = False
|
|
143
|
+
section_pattern = re.compile(r'^\s*\[\s*(?P<section_name>[^]]+)\s*\]\s*')
|
|
144
|
+
key_pattern = re.compile(rf'^\s*({re.escape(key)})\s*=\s*(.*)', re.IGNORECASE)
|
|
145
|
+
|
|
146
|
+
new_lines = []
|
|
147
|
+
for line in lines:
|
|
148
|
+
section_match = section_pattern.match(line)
|
|
149
|
+
if section_match:
|
|
150
|
+
current_section_name = section_match.group('section_name').strip()
|
|
151
|
+
if current_section_name.lower() == section.lower():
|
|
152
|
+
in_target_section = True
|
|
153
|
+
section_found = True
|
|
154
|
+
else:
|
|
155
|
+
in_target_section = False
|
|
156
|
+
new_lines.append(line)
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
if in_target_section and not key_found_in_section:
|
|
160
|
+
key_match = key_pattern.match(line)
|
|
161
|
+
if key_match:
|
|
162
|
+
original_key = key_match.group(1)
|
|
163
|
+
new_line_content = f"{original_key} = {value}\n"
|
|
164
|
+
new_lines.append(new_line_content)
|
|
165
|
+
key_found_in_section = True
|
|
166
|
+
updated = True
|
|
167
|
+
logger.info(f"Ключ '{key}' в секции '[{section}]' будет обновлен на '{value}' в файле {path}")
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
new_lines.append(line)
|
|
171
|
+
|
|
172
|
+
if not section_found:
|
|
173
|
+
logger.warning(f"Секция '[{section}]' не найдена в файле {path}. Значение НЕ сохранено.")
|
|
174
|
+
return False
|
|
175
|
+
if section_found and not key_found_in_section:
|
|
176
|
+
logger.warning(f"Ключ '{key}' не найден в секции '[{section}]' файла {path}. Значение НЕ сохранено.")
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
if updated:
|
|
180
|
+
try:
|
|
181
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
182
|
+
f.writelines(new_lines)
|
|
183
|
+
logger.info(f"Файл конфигурации {path} успешно обновлен.")
|
|
184
|
+
return True
|
|
185
|
+
except IOError as e:
|
|
186
|
+
logger.error(f"Ошибка записи в файл {path} при сохранении: {e}")
|
|
187
|
+
return False
|
|
188
|
+
else:
|
|
189
|
+
logger.debug(f"Обновление для ключа '{key}' в секции '[{section}]' не потребовалось.")
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_config() -> configparser.ConfigParser:
|
|
194
|
+
"""Возвращает полностью загруженный объект конфигурации."""
|
|
195
|
+
return load_config()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# --- Функции-обертки для удобного получения значений ---
|
|
199
|
+
|
|
200
|
+
def get_config_value(section: str, key: str, fallback: str = "", config: Optional[configparser.ConfigParser] = None) -> str:
|
|
201
|
+
"""Получает строковое значение из конфигурации."""
|
|
202
|
+
if config is None: config = load_config()
|
|
203
|
+
return config.get(section, key, fallback=fallback)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_config_int(section: str, key: str, fallback: int = 0, config: Optional[configparser.ConfigParser] = None) -> int:
|
|
207
|
+
"""Получает целочисленное значение из конфигурации."""
|
|
208
|
+
if config is None: config = load_config()
|
|
209
|
+
return config.getint(section, key, fallback=fallback)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_config_float(section: str, key: str, fallback: float = 0.0, config: Optional[configparser.ConfigParser] = None) -> float:
|
|
213
|
+
"""Получает дробное значение из конфигурации."""
|
|
214
|
+
if config is None: config = load_config()
|
|
215
|
+
return config.getfloat(section, key, fallback=fallback)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_config_boolean(section: str, key: str, fallback: bool = False, config: Optional[configparser.ConfigParser] = None) -> bool:
|
|
219
|
+
"""Получает булево значение из конфигурации."""
|
|
220
|
+
if config is None: config = load_config()
|
|
221
|
+
return config.getboolean(section, key, fallback=fallback)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_config_list(section: str, key: str, fallback: Optional[List[str]] = None, config: Optional[configparser.ConfigParser] = None) -> List[str]:
|
|
225
|
+
"""
|
|
226
|
+
Получает многострочное значение и возвращает его в виде списка очищенных строк.
|
|
227
|
+
|
|
228
|
+
Идеально подходит для списков:
|
|
229
|
+
- Разделяет значение по переносам строк.
|
|
230
|
+
- Удаляет пустые строки и лишние пробелы.
|
|
231
|
+
- Игнорирует строки, начинающиеся с '#' (комментарии).
|
|
232
|
+
"""
|
|
233
|
+
if fallback is None:
|
|
234
|
+
fallback = []
|
|
235
|
+
raw_value = get_config_value(section, key, fallback="", config=config)
|
|
236
|
+
if not raw_value:
|
|
237
|
+
return fallback
|
|
238
|
+
|
|
239
|
+
lines = [line.strip() for line in raw_value.splitlines() if line.strip() and not line.strip().startswith('#')]
|
|
240
|
+
return lines
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_multiple_config_values(section: str, keys: List[str], config: Optional[configparser.ConfigParser] = None) -> Dict[str, Optional[str]]:
|
|
244
|
+
"""Получает словарь значений для указанных ключей в секции."""
|
|
245
|
+
if config is None: config = load_config()
|
|
246
|
+
values = {}
|
|
247
|
+
if config.has_section(section):
|
|
248
|
+
for key in keys:
|
|
249
|
+
values[key] = config.get(section, key, fallback=None)
|
|
250
|
+
return values
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def get_config_section(section_name: str, fallback: Optional[Dict] = None, config: Optional[configparser.ConfigParser] = None) -> Dict[str, str]:
|
|
254
|
+
"""
|
|
255
|
+
Получает всю секцию из конфигурации как словарь.
|
|
256
|
+
"""
|
|
257
|
+
if fallback is None:
|
|
258
|
+
fallback = {}
|
|
259
|
+
if config is None:
|
|
260
|
+
config = load_config()
|
|
261
|
+
|
|
262
|
+
if config.has_section(section_name):
|
|
263
|
+
# Преобразуем секцию в обычный словарь
|
|
264
|
+
return dict(config.items(section_name))
|
|
265
|
+
else:
|
|
266
|
+
logger.warning(f"Секция '{section_name}' не найдена в конфигурации. Возвращен fallback.")
|
|
267
|
+
return fallback
|
chutils/logger.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Модуль для настройки логирования.
|
|
3
|
+
|
|
4
|
+
Предоставляет унифицированную функцию setup_logger для создания и настройки логгеров,
|
|
5
|
+
которые могут выводить сообщения в консоль и в файлы с автоматической ротацией.
|
|
6
|
+
Директория для логов ('logs') создается автоматически в корне проекта.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import logging.handlers
|
|
11
|
+
import os
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
# Импортируем наш модуль config для доступа к путям и настройкам
|
|
15
|
+
from . import config
|
|
16
|
+
|
|
17
|
+
# --- Пользовательские уровни логирования ---
|
|
18
|
+
# Для более гранулярного контроля над отладочными сообщениями.
|
|
19
|
+
|
|
20
|
+
DEVDEBUG_LEVEL_NUM = 9
|
|
21
|
+
DEVDEBUG_LEVEL_NAME = "DEVDEBUG"
|
|
22
|
+
MEDIUMDEBUG_LEVEL_NUM = 15
|
|
23
|
+
MEDIUMDEBUG_LEVEL_NAME = "MEDIUMDEBUG"
|
|
24
|
+
|
|
25
|
+
logging.addLevelName(MEDIUMDEBUG_LEVEL_NUM, MEDIUMDEBUG_LEVEL_NAME)
|
|
26
|
+
logging.addLevelName(DEVDEBUG_LEVEL_NUM, DEVDEBUG_LEVEL_NAME)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mediumdebug(self, message, *args, **kws):
|
|
30
|
+
if self.isEnabledFor(MEDIUMDEBUG_LEVEL_NUM):
|
|
31
|
+
self._log(MEDIUMDEBUG_LEVEL_NUM, message, args, **kws)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def devdebug(self, message, *args, **kws):
|
|
35
|
+
if self.isEnabledFor(DEVDEBUG_LEVEL_NUM):
|
|
36
|
+
self._log(DEVDEBUG_LEVEL_NUM, message, args, **kws)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# "Патчим" класс Logger, чтобы добавить наши кастомные методы
|
|
40
|
+
if not hasattr(logging.Logger, MEDIUMDEBUG_LEVEL_NAME.lower()):
|
|
41
|
+
logging.Logger.mediumdebug = mediumdebug
|
|
42
|
+
|
|
43
|
+
if not hasattr(logging.Logger, DEVDEBUG_LEVEL_NAME.lower()):
|
|
44
|
+
logging.Logger.devdebug = devdebug
|
|
45
|
+
|
|
46
|
+
# --- Глобальное состояние для "ленивой" инициализации ---
|
|
47
|
+
|
|
48
|
+
# Кэш для пути к директории логов. Изначально пуст.
|
|
49
|
+
_LOG_DIR: Optional[str] = None
|
|
50
|
+
# Глобальный экземпляр основного логгера приложения
|
|
51
|
+
_logger_instance: Optional[logging.Logger] = None
|
|
52
|
+
# Флаг, чтобы сообщение об инициализации выводилось только один раз
|
|
53
|
+
_initialization_message_shown = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_log_dir() -> Optional[str]:
|
|
57
|
+
"""
|
|
58
|
+
"Лениво" получает и кэширует путь к директории логов.
|
|
59
|
+
|
|
60
|
+
При первом вызове:
|
|
61
|
+
1. Запускает поиск корня проекта через модуль config.
|
|
62
|
+
2. Создает директорию 'logs' в корне проекта, если ее нет.
|
|
63
|
+
3. Кэширует результат.
|
|
64
|
+
При последующих вызовах немедленно возвращает кэшированный путь.
|
|
65
|
+
"""
|
|
66
|
+
global _LOG_DIR
|
|
67
|
+
# Если путь уже кэширован, сразу возвращаем его.
|
|
68
|
+
if _LOG_DIR is not None:
|
|
69
|
+
return _LOG_DIR
|
|
70
|
+
|
|
71
|
+
# Запускаем инициализацию в config, если она еще не была выполнена.
|
|
72
|
+
# Это "сердце" автоматического обнаружения.
|
|
73
|
+
config._initialize_paths()
|
|
74
|
+
|
|
75
|
+
# Берем найденный config'ом базовый каталог проекта.
|
|
76
|
+
base_dir = config._BASE_DIR
|
|
77
|
+
|
|
78
|
+
# Если корень проекта не был найден, файловое логирование невозможно.
|
|
79
|
+
if not base_dir:
|
|
80
|
+
print("ПРЕДУПРЕЖДЕНИЕ: Не удалось определить корень проекта, файловое логирование будет отключено.")
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
# Создаем путь к директории логов и саму директорию, если нужно.
|
|
84
|
+
log_path = os.path.join(base_dir, 'logs')
|
|
85
|
+
if not os.path.exists(log_path):
|
|
86
|
+
try:
|
|
87
|
+
os.makedirs(log_path)
|
|
88
|
+
print(f"INFO: Создана директория для логов: {log_path}")
|
|
89
|
+
except OSError as e:
|
|
90
|
+
# Если не удалось создать директорию, логирование в файл будет невозможно.
|
|
91
|
+
print(f"ОШИБКА: Не удалось создать директорию для логов {log_path}: {e}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Кэшируем успешный результат и возвращаем его.
|
|
95
|
+
_LOG_DIR = log_path
|
|
96
|
+
return _LOG_DIR
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def setup_logger(name: str = 'app_logger', log_level_str: str = '') -> logging.Logger:
|
|
100
|
+
"""
|
|
101
|
+
Настраивает и возвращает логгер с нужным именем.
|
|
102
|
+
|
|
103
|
+
- Предотвращает повторную настройку уже существующего логгера.
|
|
104
|
+
- Читает настройки из config.ini (уровень, имя файла и т.д.).
|
|
105
|
+
- Добавляет обработчики для вывода в консоль и в файл с ежедневной ротацией.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
name (str): Имя логгера. 'app_logger' используется для основного логгера приложения.
|
|
109
|
+
log_level_str (str, optional): Явное указание уровня логирования (например, 'DEBUG').
|
|
110
|
+
Если не задан, берется из конфига.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
logging.Logger: Настроенный экземпляр логгера.
|
|
114
|
+
"""
|
|
115
|
+
global _logger_instance, _initialization_message_shown
|
|
116
|
+
|
|
117
|
+
# Если логгер с таким именем уже имеет обработчики, значит он настроен.
|
|
118
|
+
# Просто возвращаем его, чтобы не дублировать вывод.
|
|
119
|
+
existing_logger = logging.getLogger(name)
|
|
120
|
+
if existing_logger.hasHandlers():
|
|
121
|
+
return existing_logger
|
|
122
|
+
|
|
123
|
+
# Если запрашивается основной логгер приложения и он уже есть в кэше.
|
|
124
|
+
if name == 'app_logger' and _logger_instance:
|
|
125
|
+
return _logger_instance
|
|
126
|
+
|
|
127
|
+
# Получаем директорию для логов. Это первая точка, где запускается вся магия поиска путей.
|
|
128
|
+
log_dir = _get_log_dir()
|
|
129
|
+
|
|
130
|
+
# Загружаем конфигурацию для получения настроек логирования.
|
|
131
|
+
cfg = config.get_config()
|
|
132
|
+
|
|
133
|
+
# Определяем уровень логирования
|
|
134
|
+
if not log_level_str:
|
|
135
|
+
log_level_str = config.get_config_value('Logging', 'log_level', 'INFO', cfg)
|
|
136
|
+
log_level = getattr(logging, log_level_str.upper(), logging.INFO)
|
|
137
|
+
|
|
138
|
+
# Получаем остальные настройки из конфига
|
|
139
|
+
log_file_name = config.get_config_value('Logging', 'log_file_name', 'app.log', cfg)
|
|
140
|
+
backup_count = config.get_config_int('Logging', 'log_backup_count', 3, cfg)
|
|
141
|
+
|
|
142
|
+
# Создаем и настраиваем новый экземпляр логгера
|
|
143
|
+
logger = logging.getLogger(name)
|
|
144
|
+
logger.setLevel(log_level)
|
|
145
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
146
|
+
|
|
147
|
+
# 1. Обработчик для вывода в консоль (StreamHandler)
|
|
148
|
+
console_handler = logging.StreamHandler()
|
|
149
|
+
console_handler.setFormatter(formatter)
|
|
150
|
+
logger.addHandler(console_handler)
|
|
151
|
+
|
|
152
|
+
# 2. Обработчик для записи в файл (TimedRotatingFileHandler)
|
|
153
|
+
# Добавляем его, только если директория логов была успешно определена.
|
|
154
|
+
if log_dir and log_file_name:
|
|
155
|
+
log_file_path = os.path.join(log_dir, log_file_name)
|
|
156
|
+
try:
|
|
157
|
+
# Ротация каждый день ('D'), храним backup_count старых файлов
|
|
158
|
+
file_handler = logging.handlers.TimedRotatingFileHandler(
|
|
159
|
+
log_file_path,
|
|
160
|
+
when="D",
|
|
161
|
+
interval=1,
|
|
162
|
+
backupCount=backup_count,
|
|
163
|
+
encoding='utf-8'
|
|
164
|
+
)
|
|
165
|
+
file_handler.setFormatter(formatter)
|
|
166
|
+
logger.addHandler(file_handler)
|
|
167
|
+
|
|
168
|
+
# Выводим информационное сообщение только один раз для всего приложения
|
|
169
|
+
if not _initialization_message_shown:
|
|
170
|
+
logger.info(
|
|
171
|
+
f"Логирование настроено. Уровень: {log_level_str}. "
|
|
172
|
+
f"Файл: {log_file_path}, ротация: {backup_count} дней."
|
|
173
|
+
)
|
|
174
|
+
_initialization_message_shown = True
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Не удалось настроить файловый обработчик логов для {log_file_path}: {e}")
|
|
177
|
+
else:
|
|
178
|
+
if not _initialization_message_shown:
|
|
179
|
+
logger.warning("Директория для логов не настроена. Файловое логирование отключено.")
|
|
180
|
+
_initialization_message_shown = True
|
|
181
|
+
|
|
182
|
+
# Кэшируем основной логгер приложения
|
|
183
|
+
if name == 'app_logger':
|
|
184
|
+
_logger_instance = logger
|
|
185
|
+
|
|
186
|
+
return logger
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chu4hel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: chutils
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary:
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Sergo
|
|
7
|
+
Author-email: sergeiivanov636@gmail.com
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# chutils
|
|
20
|
+
|
|
21
|
+
[](https://opensource.org/licenses/MIT) [](https://www.python.org/downloads/)
|
|
22
|
+
|
|
23
|
+
Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах.
|
|
24
|
+
|
|
25
|
+
## Проблема
|
|
26
|
+
|
|
27
|
+
Каждый раз, начиная новый проект, приходится решать одни и те же задачи:
|
|
28
|
+
- Как удобно читать настройки из файла `config.ini`?
|
|
29
|
+
- Как настроить логирование, чтобы сообщения писались и в консоль, и в файл с ежедневной ротацией?
|
|
30
|
+
- Как сделать так, чтобы это работало без жестко прописанных путей и работало сразу после установки?
|
|
31
|
+
|
|
32
|
+
**chutils** решает эти проблемы.
|
|
33
|
+
|
|
34
|
+
## Ключевые возможности
|
|
35
|
+
|
|
36
|
+
- **✨ Ноль конфигурации:** Библиотека **автоматически** находит корень вашего проекта и файл `config.ini`. Вам не нужно ничего инициализировать вручную.
|
|
37
|
+
- **⚙️ Удобная работа с конфигом:** Простые функции для получения строковых, числовых, булевых значений и даже списков из `config.ini`.
|
|
38
|
+
- **✍️ Мощный логгер:** Функция `setup_logger()` "из коробки" настраивает логирование в консоль и в ротируемые файлы в папке `logs/`, которая создается автоматически.
|
|
39
|
+
- **🚀 Готовность к работе:** Просто установите и используйте.
|
|
40
|
+
|
|
41
|
+
## Установка
|
|
42
|
+
|
|
43
|
+
Вы можете установить пакет напрямую из GitHub-репозитория с помощью `pip`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://github.com/Chu4hel/chutils.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Для разработки клонируйте репозиторий и установите его в режиме редактирования:
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/Chu4hel/chutils.git
|
|
52
|
+
cd chutils
|
|
53
|
+
pip install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Быстрый старт
|
|
57
|
+
|
|
58
|
+
1. Создайте в корне вашего проекта файл `config.ini`.
|
|
59
|
+
|
|
60
|
+
**Структура проекта:**
|
|
61
|
+
```
|
|
62
|
+
my_awesome_app/
|
|
63
|
+
├── main.py
|
|
64
|
+
└── config.ini
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Содержимое `config.ini`:**
|
|
68
|
+
```ini
|
|
69
|
+
[API]
|
|
70
|
+
base_url = https://api.example.com
|
|
71
|
+
token = your_secret_token_here
|
|
72
|
+
|
|
73
|
+
[Database]
|
|
74
|
+
host = localhost
|
|
75
|
+
port = 5432
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
2. Используйте `chutils` в вашем коде `main.py`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# main.py
|
|
82
|
+
from chutils.config import get_config_value
|
|
83
|
+
from chutils.logger import setup_logger
|
|
84
|
+
|
|
85
|
+
# 1. Настраиваем логгер. Он автоматически прочитает настройки из config.ini
|
|
86
|
+
# и создаст папку logs/
|
|
87
|
+
logger = setup_logger()
|
|
88
|
+
|
|
89
|
+
def connect_to_db():
|
|
90
|
+
# 2. Легко получаем значения из конфига
|
|
91
|
+
db_host = get_config_value("Database", "host")
|
|
92
|
+
db_port = get_config_value("Database", "port")
|
|
93
|
+
|
|
94
|
+
logger.info(f"Подключаемся к базе данных по адресу {db_host}:{db_port}...")
|
|
95
|
+
# ... логика подключения ...
|
|
96
|
+
logger.info("Успешно подключились!")
|
|
97
|
+
|
|
98
|
+
def main():
|
|
99
|
+
logger.info("Приложение запущено.")
|
|
100
|
+
connect_to_db()
|
|
101
|
+
api_token = get_config_value("API", "token")
|
|
102
|
+
logger.debug(f"Используемый токен API: {api_token[:4]}****")
|
|
103
|
+
logger.info("Приложение завершило работу.")
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
3. Запустите ваш скрипт. Вы увидите логи в консоли, а в проекте появится папка `logs` с файлом лога.
|
|
110
|
+
|
|
111
|
+
## API и Использование
|
|
112
|
+
|
|
113
|
+
### Работа с конфигурацией (`chutils.config`)
|
|
114
|
+
|
|
115
|
+
- `get_config_value(section, key, fallback="")`: Получить строковое значение.
|
|
116
|
+
- `get_config_int(section, key, fallback=0)`: Получить целое число.
|
|
117
|
+
- `get_config_boolean(section, key, fallback=False)`: Получить булево значение.
|
|
118
|
+
- `get_config_list(section, key, fallback=[])`: Получить список строк из многострочного значения.
|
|
119
|
+
- `save_config_value(section, key, value)`: Сохранить значение в `config.ini`.
|
|
120
|
+
|
|
121
|
+
### Настройка логирования (`chutils.logger`)
|
|
122
|
+
|
|
123
|
+
- `setup_logger(name='app_logger', log_level_str='')`: Настраивает и возвращает стандартный объект `logging.Logger`.
|
|
124
|
+
|
|
125
|
+
### Ручная инициализация (`chutils.init`)
|
|
126
|
+
|
|
127
|
+
В 99% случаев вам это **не понадобится**. Но если автоматика не справилась, вы можете один раз указать путь к проекту вручную:
|
|
128
|
+
```python
|
|
129
|
+
import chutils
|
|
130
|
+
chutils.init(base_dir="/path/to/my/project/root")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Пример файла `config.ini`
|
|
134
|
+
|
|
135
|
+
`chutils` использует секцию `[Logging]` для настройки логгера.
|
|
136
|
+
|
|
137
|
+
```ini
|
|
138
|
+
[API]
|
|
139
|
+
token = your_secret_token_here
|
|
140
|
+
|
|
141
|
+
[Database]
|
|
142
|
+
host = localhost
|
|
143
|
+
|
|
144
|
+
[Logging]
|
|
145
|
+
# Уровни: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
146
|
+
log_level = DEBUG
|
|
147
|
+
# Имя файла для логов
|
|
148
|
+
log_file_name = my_app.log
|
|
149
|
+
# Сколько дней хранить файлы логов
|
|
150
|
+
log_backup_count = 7
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Лицензия
|
|
154
|
+
|
|
155
|
+
Проект распространяется под лицензией MIT.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
chutils/__init__.py,sha256=PJ7vusg09BA87CH46PQuMuzqN_2fdmW3DZwzz5g34CU,5395
|
|
2
|
+
chutils/config.py,sha256=suN_1LDY3Jn50bLIzCXyng9snhluhIn4a_TG1Uumepo,12932
|
|
3
|
+
chutils/logger.py,sha256=BUc76gvTyjmsiD6niA7CSMJ6I7RweRCEJA90sBc2z_c,9596
|
|
4
|
+
chutils-1.0.0.dist-info/LICENSE,sha256=S3egx-rPyEfAdXQl2ybrzfOJmUDlK3KBP2GqroTvm9o,1085
|
|
5
|
+
chutils-1.0.0.dist-info/METADATA,sha256=S0RREF-meeVsqmR0X0p4imfDQ3uX8ctjvNx0IhZHYL0,6702
|
|
6
|
+
chutils-1.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
7
|
+
chutils-1.0.0.dist-info/RECORD,,
|