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 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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python](https://img.shields.io/badge/python-3.8%2B-blue.svg)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any