chutils 2.0.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.0.0/LICENSE ADDED
@@ -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.
chutils-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.3
2
+ Name: chutils
3
+ Version: 2.0.0
4
+ Summary:
5
+ License: MIT
6
+ Author: Sergo
7
+ Author-email: sergeiivanov636@gmail.com
8
+ Requires-Python: >=3.9
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: keyring (>=25.6.0,<26.0.0)
17
+ Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # chutils
21
+
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/downloads/)
23
+
24
+ Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией, логированием и
25
+ секретами в новых проектах.
26
+
27
+ ## Проблема
28
+
29
+ Каждый раз, начиная новый проект, приходится решать одни и те же задачи:
30
+
31
+ - Как удобно читать настройки из файла конфигурации?
32
+ - Как настроить логирование, чтобы сообщения писались и в консоль, и в файл с ежедневной ротацией?
33
+ - Как безопасно хранить API-ключи и пароли, не записывая их в код или в файлы конфигурации?
34
+ - Как сделать так, чтобы все это работало без жестко прописанных путей сразу после установки?
35
+
36
+ **chutils** решает эти проблемы.
37
+
38
+ ## Ключевые возможности
39
+
40
+ - **✨ Ноль конфигурации:** Библиотека **автоматически** находит корень вашего проекта и файл конфигурации (`config.yml`
41
+ или `config.ini`).
42
+ - **⚙️ Гибкая конфигурация:** Поддержка `YAML` и `INI` форматов. Простые функции для получения типизированных данных.
43
+ - **✍️ Продвинутый логгер:** Функция `setup_logger()` "из коробки" настраивает логирование в консоль и в ротируемые
44
+ файлы. Возвращает кастомный логгер с дополнительными уровнями отладки (`devdebug`, `mediumdebug`).
45
+ - **🔒 Безопасное хранилище секретов:** Модуль `secret_manager` предоставляет простой интерфейс для сохранения и
46
+ получения секретов через системное хранилище ключей (Keyring).
47
+ - **🚀 Готовность к работе:** Просто установите и используйте.
48
+
49
+ ## Установка
50
+
51
+ ```bash
52
+ poetry add chutils
53
+ ```
54
+
55
+ Или с помощью pip:
56
+
57
+ ```bash
58
+ pip install chutils
59
+ ```
60
+
61
+ Для разработки клонируйте репозиторий и установите его в режиме редактирования:
62
+
63
+ ```bash
64
+ git clone https://github.com/Chu4hel/chutils.git
65
+ cd chutils
66
+ pip install -e .
67
+ ```
68
+
69
+ ## Быстрый старт
70
+
71
+ 1. Создайте в корне вашего проекта файл `config.yml`.
72
+
73
+ **Структура проекта:**
74
+ ```
75
+ my_awesome_app/
76
+ ├── main.py
77
+ └── config.yml
78
+ ```
79
+
80
+ **Содержимое `config.yml`:**
81
+ ```yaml
82
+ API:
83
+ base_url: https://api.example.com
84
+
85
+ Database:
86
+ host: localhost
87
+ port: 5432
88
+ user: my_user
89
+ ```
90
+
91
+ 2. Используйте `chutils` в вашем коде `main.py`:
92
+
93
+ ```python
94
+ # main.py
95
+ from chutils import get_config_value, setup_logger, SecretManager, ChutilsLogger
96
+
97
+ # 1. Настраиваем логгер. Он автоматически прочитает настройки из конфига.
98
+ logger: ChutilsLogger = setup_logger()
99
+
100
+ # 2. Инициализируем менеджер секретов для нашего приложения.
101
+ secrets = SecretManager("my_awesome_app")
102
+
103
+ def setup_credentials():
104
+ """Функция для первоначального сохранения пароля."""
105
+ db_user = get_config_value("Database", "user")
106
+ if not secrets.get_secret(f"{db_user}_password"):
107
+ logger.info("Пароль для БД не найден. Сохраняем новый пароль...")
108
+ secrets.save_secret(f"{db_user}_password", "MySuperSecretDbPassword123!")
109
+ logger.info("Пароль для БД сохранен в системном хранилище.")
110
+
111
+ def connect_to_db():
112
+ # 3. Легко получаем значения из конфига и секреты из хранилища.
113
+ db_host = get_config_value("Database", "host")
114
+ db_user = get_config_value("Database", "user")
115
+ db_password = secrets.get_secret(f"{db_user}_password")
116
+
117
+ if not db_password:
118
+ logger.error("Не удалось получить пароль для БД!")
119
+ return
120
+
121
+ logger.info(f"Подключаемся к базе данных по адресу {db_host} от имени {db_user}...")
122
+ # ... логика подключения ...
123
+ logger.info("Успешно подключились!")
124
+
125
+ def main():
126
+ logger.info("Приложение запущено.")
127
+ setup_credentials()
128
+ connect_to_db()
129
+ logger.info("Приложение завершило работу.")
130
+
131
+ if __name__ == "__main__":
132
+ main()
133
+ ```
134
+
135
+ 3. Запустите ваш скрипт. Вы увидите логи в консоли, а в проекте появится папка `logs` с файлом лога. Пароль от БД будет
136
+ надежно сохранен в системном хранилище.
137
+
138
+ ## API и Использование
139
+
140
+ ### Работа с конфигурацией (`chutils.config`)
141
+
142
+ - `get_config_value(section, key, fallback="")`: Получить значение.
143
+ - `get_config_int(section, key, fallback=0)`: Получить целое число.
144
+ - `get_config_boolean(section, key, fallback=False)`: Получить булево значение.
145
+ - `get_config_list(section, key, fallback=[])`: Получить список.
146
+ - `get_config_section(section)`: Получить всю секцию как словарь.
147
+ - `save_config_value(section, key, value)`: Сохранить значение. **Важно: работает только для `.ini` файлов** для
148
+ сохранения комментариев.
149
+
150
+ ### Настройка логирования (`chutils.logger`)
151
+
152
+ - `setup_logger(name='app_logger', log_level_str='')`: Настраивает и возвращает экземпляр `ChutilsLogger`.
153
+ - `logger.mediumdebug("message")`: Логирование с уровнем 15.
154
+ - `logger.devdebug("message")`: Логирование с уровнем 9.
155
+
156
+ ### Управление секретами (`chutils.secret_manager`)
157
+
158
+ - `SecretManager(service_name)`: Создает менеджер, изолированный по имени сервиса.
159
+ - `secrets.save_secret(key, value)`: Сохраняет секрет.
160
+ - `secrets.get_secret(key)`: Получает секрет.
161
+ - `secrets.delete_secret(key)`: Удаляет секрет.
162
+
163
+ ### Ручная инициализация (`chutils.init`)
164
+
165
+ В 99% случаев вам это **не понадобится**. Но если автоматика не справилась, вы можете один раз указать путь к проекту
166
+ вручную:
167
+
168
+ ```python
169
+ import chutils
170
+
171
+ chutils.init(base_dir="/path/to/my/project/root")
172
+ ```
173
+
174
+ ### Пример файла `config.yml`
175
+
176
+ `chutils` использует секцию `Logging` для настройки логгера.
177
+
178
+ ```yaml
179
+ API:
180
+ token: your_secret_token_here
181
+
182
+ Database:
183
+ host: localhost
184
+
185
+ Logging:
186
+ # Уровни: DEVDEBUG, DEBUG, MEDIUMDEBUG, INFO, WARNING, ERROR, CRITICAL
187
+ log_level: DEBUG
188
+ # Имя файла для логов
189
+ log_file_name: my_app.log
190
+ # Сколько дней хранить файлы логов
191
+ log_backup_count: 7
192
+ ```
193
+
194
+ ## Лицензия
195
+
196
+ Проект распространяется под лицензией MIT.
@@ -0,0 +1,177 @@
1
+ # chutils
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/downloads/)
4
+
5
+ Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией, логированием и
6
+ секретами в новых проектах.
7
+
8
+ ## Проблема
9
+
10
+ Каждый раз, начиная новый проект, приходится решать одни и те же задачи:
11
+
12
+ - Как удобно читать настройки из файла конфигурации?
13
+ - Как настроить логирование, чтобы сообщения писались и в консоль, и в файл с ежедневной ротацией?
14
+ - Как безопасно хранить API-ключи и пароли, не записывая их в код или в файлы конфигурации?
15
+ - Как сделать так, чтобы все это работало без жестко прописанных путей сразу после установки?
16
+
17
+ **chutils** решает эти проблемы.
18
+
19
+ ## Ключевые возможности
20
+
21
+ - **✨ Ноль конфигурации:** Библиотека **автоматически** находит корень вашего проекта и файл конфигурации (`config.yml`
22
+ или `config.ini`).
23
+ - **⚙️ Гибкая конфигурация:** Поддержка `YAML` и `INI` форматов. Простые функции для получения типизированных данных.
24
+ - **✍️ Продвинутый логгер:** Функция `setup_logger()` "из коробки" настраивает логирование в консоль и в ротируемые
25
+ файлы. Возвращает кастомный логгер с дополнительными уровнями отладки (`devdebug`, `mediumdebug`).
26
+ - **🔒 Безопасное хранилище секретов:** Модуль `secret_manager` предоставляет простой интерфейс для сохранения и
27
+ получения секретов через системное хранилище ключей (Keyring).
28
+ - **🚀 Готовность к работе:** Просто установите и используйте.
29
+
30
+ ## Установка
31
+
32
+ ```bash
33
+ poetry add chutils
34
+ ```
35
+
36
+ Или с помощью pip:
37
+
38
+ ```bash
39
+ pip install chutils
40
+ ```
41
+
42
+ Для разработки клонируйте репозиторий и установите его в режиме редактирования:
43
+
44
+ ```bash
45
+ git clone https://github.com/Chu4hel/chutils.git
46
+ cd chutils
47
+ pip install -e .
48
+ ```
49
+
50
+ ## Быстрый старт
51
+
52
+ 1. Создайте в корне вашего проекта файл `config.yml`.
53
+
54
+ **Структура проекта:**
55
+ ```
56
+ my_awesome_app/
57
+ ├── main.py
58
+ └── config.yml
59
+ ```
60
+
61
+ **Содержимое `config.yml`:**
62
+ ```yaml
63
+ API:
64
+ base_url: https://api.example.com
65
+
66
+ Database:
67
+ host: localhost
68
+ port: 5432
69
+ user: my_user
70
+ ```
71
+
72
+ 2. Используйте `chutils` в вашем коде `main.py`:
73
+
74
+ ```python
75
+ # main.py
76
+ from chutils import get_config_value, setup_logger, SecretManager, ChutilsLogger
77
+
78
+ # 1. Настраиваем логгер. Он автоматически прочитает настройки из конфига.
79
+ logger: ChutilsLogger = setup_logger()
80
+
81
+ # 2. Инициализируем менеджер секретов для нашего приложения.
82
+ secrets = SecretManager("my_awesome_app")
83
+
84
+ def setup_credentials():
85
+ """Функция для первоначального сохранения пароля."""
86
+ db_user = get_config_value("Database", "user")
87
+ if not secrets.get_secret(f"{db_user}_password"):
88
+ logger.info("Пароль для БД не найден. Сохраняем новый пароль...")
89
+ secrets.save_secret(f"{db_user}_password", "MySuperSecretDbPassword123!")
90
+ logger.info("Пароль для БД сохранен в системном хранилище.")
91
+
92
+ def connect_to_db():
93
+ # 3. Легко получаем значения из конфига и секреты из хранилища.
94
+ db_host = get_config_value("Database", "host")
95
+ db_user = get_config_value("Database", "user")
96
+ db_password = secrets.get_secret(f"{db_user}_password")
97
+
98
+ if not db_password:
99
+ logger.error("Не удалось получить пароль для БД!")
100
+ return
101
+
102
+ logger.info(f"Подключаемся к базе данных по адресу {db_host} от имени {db_user}...")
103
+ # ... логика подключения ...
104
+ logger.info("Успешно подключились!")
105
+
106
+ def main():
107
+ logger.info("Приложение запущено.")
108
+ setup_credentials()
109
+ connect_to_db()
110
+ logger.info("Приложение завершило работу.")
111
+
112
+ if __name__ == "__main__":
113
+ main()
114
+ ```
115
+
116
+ 3. Запустите ваш скрипт. Вы увидите логи в консоли, а в проекте появится папка `logs` с файлом лога. Пароль от БД будет
117
+ надежно сохранен в системном хранилище.
118
+
119
+ ## API и Использование
120
+
121
+ ### Работа с конфигурацией (`chutils.config`)
122
+
123
+ - `get_config_value(section, key, fallback="")`: Получить значение.
124
+ - `get_config_int(section, key, fallback=0)`: Получить целое число.
125
+ - `get_config_boolean(section, key, fallback=False)`: Получить булево значение.
126
+ - `get_config_list(section, key, fallback=[])`: Получить список.
127
+ - `get_config_section(section)`: Получить всю секцию как словарь.
128
+ - `save_config_value(section, key, value)`: Сохранить значение. **Важно: работает только для `.ini` файлов** для
129
+ сохранения комментариев.
130
+
131
+ ### Настройка логирования (`chutils.logger`)
132
+
133
+ - `setup_logger(name='app_logger', log_level_str='')`: Настраивает и возвращает экземпляр `ChutilsLogger`.
134
+ - `logger.mediumdebug("message")`: Логирование с уровнем 15.
135
+ - `logger.devdebug("message")`: Логирование с уровнем 9.
136
+
137
+ ### Управление секретами (`chutils.secret_manager`)
138
+
139
+ - `SecretManager(service_name)`: Создает менеджер, изолированный по имени сервиса.
140
+ - `secrets.save_secret(key, value)`: Сохраняет секрет.
141
+ - `secrets.get_secret(key)`: Получает секрет.
142
+ - `secrets.delete_secret(key)`: Удаляет секрет.
143
+
144
+ ### Ручная инициализация (`chutils.init`)
145
+
146
+ В 99% случаев вам это **не понадобится**. Но если автоматика не справилась, вы можете один раз указать путь к проекту
147
+ вручную:
148
+
149
+ ```python
150
+ import chutils
151
+
152
+ chutils.init(base_dir="/path/to/my/project/root")
153
+ ```
154
+
155
+ ### Пример файла `config.yml`
156
+
157
+ `chutils` использует секцию `Logging` для настройки логгера.
158
+
159
+ ```yaml
160
+ API:
161
+ token: your_secret_token_here
162
+
163
+ Database:
164
+ host: localhost
165
+
166
+ Logging:
167
+ # Уровни: DEVDEBUG, DEBUG, MEDIUMDEBUG, INFO, WARNING, ERROR, CRITICAL
168
+ log_level: DEBUG
169
+ # Имя файла для логов
170
+ log_file_name: my_app.log
171
+ # Сколько дней хранить файлы логов
172
+ log_backup_count: 7
173
+ ```
174
+
175
+ ## Лицензия
176
+
177
+ Проект распространяется под лицензией MIT.
@@ -0,0 +1,28 @@
1
+ [tool.poetry]
2
+ name = "chutils"
3
+ version = "2.0.0"
4
+ description = ""
5
+ authors = ["Sergo <sergeiivanov636@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ packages = [{include = "chutils", from = "src"}]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.9"
12
+ keyring = "^25.6.0"
13
+ pyyaml = "^6.0.3"
14
+
15
+
16
+ [tool.poetry.group.dev.dependencies]
17
+ pytest = "^8.4.2"
18
+ pytest-mock = "^3.15.1"
19
+ pyfakefs = "^5.10.0"
20
+
21
+ [build-system]
22
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
23
+ build-backend = "poetry.core.masonry.api"
24
+
25
+ [tool.pytest.ini_options]
26
+ pythonpath = [
27
+ "src"
28
+ ]
@@ -0,0 +1,102 @@
1
+ """
2
+ Пакет chutils - набор переиспользуемых утилит для Python.
3
+
4
+ Основная цель - упростить рутинные задачи, такие как работа с конфигурацией,
5
+ логированием и управлением секретами, с минимальными усилиями со стороны разработчика.
6
+
7
+ Ключевые особенности:
8
+ - Автоматическое обнаружение корня проекта и файла конфигурации.
9
+ - Поддержка форматов `config.yml`, `config.yaml` и `config.ini` (YAML в приоритете).
10
+ - Удобные функции для доступа к настройкам.
11
+ - Готовый к работе логгер с выводом в консоль и ротируемые файлы.
12
+ - Безопасное хранение секретов через системное хранилище (keyring).
13
+
14
+ Основное использование:
15
+ ----------------------
16
+ Вам не нужно ничего инициализировать. Просто импортируйте и используйте:
17
+
18
+ from chutils import get_config_value, setup_logger, SecretManager
19
+
20
+ logger = setup_logger()
21
+ secrets = SecretManager("my_app")
22
+ db_host = get_config_value("Database", "host", "localhost")
23
+ logger.info(f"Подключение к базе данных на {db_host}")
24
+
25
+ Ручная инициализация (для нестандартных случаев):
26
+ -------------------------------------------------
27
+ Если автоматика не сработала, вы можете указать путь к корню проекта вручную:
28
+
29
+ import chutils
30
+ chutils.init(base_dir="/path/to/your/project")
31
+
32
+ """
33
+
34
+ import os
35
+
36
+ from . import config
37
+ from . import logger
38
+
39
+ # --- Импорт публичных функций и классов ---
40
+ # Явно импортируем все, что должно быть доступно пользователю напрямую из пакета chutils.
41
+
42
+ from .config import (
43
+ get_config,
44
+ get_config_value,
45
+ get_config_int,
46
+ get_config_float,
47
+ get_config_boolean,
48
+ get_config_list,
49
+ get_config_section
50
+ )
51
+ from .logger import setup_logger, ChutilsLogger
52
+ from .secret_manager import SecretManager
53
+
54
+
55
+ def init(base_dir: str):
56
+ """
57
+ Ручная инициализация пакета с указанием базовой директории проекта.
58
+
59
+ Эту функцию нужно вызывать только в том случае, если автоматическое
60
+ определение корня проекта не сработало. Вызывать следует один раз
61
+ в самом начале работы основного скрипта вашего приложения.
62
+
63
+ Args:
64
+ base_dir (str): Абсолютный путь к корневой директории проекта.
65
+
66
+ Raises:
67
+ ValueError: Если указанная директория не существует.
68
+ """
69
+ if not os.path.isdir(base_dir):
70
+ raise ValueError(f"Указанная директория base_dir не существует или не является директорией: {base_dir}")
71
+
72
+ # Вручную устанавливаем базовую директорию. Модуль config сам найдет
73
+ # нужный файл (yml или ini) при первом обращении.
74
+ config._BASE_DIR = base_dir
75
+ config._paths_initialized = True
76
+
77
+ print(f"Пакет chutils вручную инициализирован с базовой директорией: {base_dir}")
78
+
79
+
80
+ # --- Определение публичного API (`__all__`) ---
81
+ # Определяет, что будет импортировано при `from chutils import *`
82
+
83
+ __all__ = [
84
+ # Основная функция ручной инициализации
85
+ 'init',
86
+
87
+ # Функции и классы из модуля config
88
+ 'get_config',
89
+ 'get_config_value',
90
+ 'get_config_int',
91
+ 'get_config_float',
92
+ 'get_config_boolean',
93
+ 'get_config_list',
94
+ 'get_config_section',
95
+
96
+ # Функции и классы из модуля logger
97
+ 'setup_logger',
98
+ 'ChutilsLogger',
99
+
100
+ # Классы из модуля secret_manager
101
+ 'SecretManager',
102
+ ]
@@ -0,0 +1,277 @@
1
+ """
2
+ Модуль для работы с конфигурацией.
3
+
4
+ Обеспечивает автоматический поиск файла `config.yml`, `config.yaml` или `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
+ import yaml
16
+
17
+ # Настраиваем логгер для этого модуля
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # --- Глобальное состояние для "ленивой" инициализации ---
21
+ _BASE_DIR: Optional[str] = None
22
+ _CONFIG_FILE_PATH: Optional[str] = None
23
+ _paths_initialized = False
24
+
25
+ _config_object: Optional[Dict] = None
26
+ _config_loaded = False
27
+
28
+
29
+ def find_project_root(start_path: Path, markers: List[str]) -> Optional[Path]:
30
+ """Ищет корень проекта, двигаясь вверх по дереву каталогов."""
31
+ current_path = start_path.resolve()
32
+ # Идем вверх до тех пор, пока не достигнем корня файловой системы
33
+ while current_path != current_path.parent:
34
+ for marker in markers:
35
+ if (current_path / marker).exists():
36
+ logger.debug(f"Найден маркер '{marker}' в директории: {current_path}")
37
+ return current_path
38
+ current_path = current_path.parent
39
+ logger.debug("Корень проекта не найден.")
40
+ return None
41
+
42
+
43
+ def _initialize_paths():
44
+ """Автоматически находит и кэширует пути к корню проекта и файлу конфигурации."""
45
+ global _BASE_DIR, _CONFIG_FILE_PATH, _paths_initialized
46
+ if _paths_initialized:
47
+ return
48
+
49
+ # Приоритет поиска: сначала YAML, потом INI, потом общий маркер проекта.
50
+ markers = ['config.yml', 'config.yaml', 'config.ini', 'pyproject.toml']
51
+ project_root = find_project_root(Path.cwd(), markers)
52
+
53
+ if project_root:
54
+ _BASE_DIR = str(project_root)
55
+ # Находим, какой именно конфигурационный файл был найден
56
+ for marker in markers:
57
+ if (project_root / marker).is_file() and marker.startswith('config'):
58
+ _CONFIG_FILE_PATH = str(project_root / marker)
59
+ break
60
+ logger.info(f"Корень проекта автоматически определен: {_BASE_DIR}")
61
+ else:
62
+ logger.warning("Не удалось автоматически найти корень проекта.")
63
+
64
+ _paths_initialized = True
65
+
66
+
67
+ def _get_config_path(cfg_file: Optional[str] = None) -> str:
68
+ """
69
+ Внутренняя функция-шлюз для получения пути к файлу конфигурации.
70
+
71
+ Если путь не был установлен, запускает автоматический поиск.
72
+ Если путь не передан явно и автоматический поиск не дал результатов,
73
+ выбрасывает исключение с понятным сообщением.
74
+ """
75
+ # Если путь к файлу передан явно, используем его.
76
+ if cfg_file:
77
+ return cfg_file
78
+
79
+ # Если пути еще не инициализированы, запускаем поиск.
80
+ if not _paths_initialized:
81
+ _initialize_paths()
82
+
83
+ # Если после инициализации путь все еще не определен, это ошибка.
84
+ if _CONFIG_FILE_PATH is None:
85
+ raise FileNotFoundError(
86
+ "Файл конфигурации не найден. Не удалось автоматически определить корень проекта. "
87
+ "Убедитесь, что в корне вашего проекта есть 'config.yml' или 'config.ini' или 'pyproject.toml', "
88
+ "либо укажите путь к конфигу вручную через chutils.init(base_dir=...)"
89
+ )
90
+ return _CONFIG_FILE_PATH
91
+
92
+
93
+ def get_config() -> Dict:
94
+ """
95
+ Загружает конфигурацию из файла (YAML или INI) и возвращает ее как словарь.
96
+ Результат кэшируется для последующих вызовов.
97
+
98
+ Returns:
99
+ Dict: Загруженный объект конфигурации.
100
+ """
101
+ global _config_object, _config_loaded
102
+ if _config_loaded and _config_object is not None:
103
+ return _config_object
104
+
105
+ path = _get_config_path()
106
+ if not os.path.exists(path):
107
+ logger.critical(f"Файл конфигурации НЕ НАЙДЕН: {path}")
108
+ _config_object = {}
109
+ _config_loaded = True
110
+ return _config_object
111
+
112
+ file_ext = Path(path).suffix.lower()
113
+
114
+ try:
115
+ with open(path, 'r', encoding='utf-8') as f:
116
+ if file_ext in ['.yml', '.yaml']:
117
+ _config_object = yaml.safe_load(f)
118
+ logger.info(f"Конфигурация успешно загружена из YAML: {path}")
119
+ elif file_ext == '.ini':
120
+ parser = configparser.ConfigParser()
121
+ parser.read_string(f.read())
122
+ # Преобразуем объект ConfigParser в словарь
123
+ _config_object = {s: dict(parser.items(s)) for s in parser.sections()}
124
+ logger.info(f"Конфигурация успешно загружена из INI: {path}")
125
+ else:
126
+ _config_object = {}
127
+ logger.warning(f"Неподдерживаемый формат файла конфигурации: {path}")
128
+
129
+ except (yaml.YAMLError, configparser.Error) as e:
130
+ logger.critical(f"Ошибка чтения файла конфигурации {path}: {e}")
131
+ _config_object = {}
132
+
133
+ if _config_object is None:
134
+ _config_object = {}
135
+
136
+ _config_loaded = True
137
+ return _config_object
138
+
139
+
140
+ def save_config_value(section: str, key: str, value: str, cfg_file: Optional[str] = None) -> bool:
141
+ """
142
+ Сохраняет одно значение в конфигурационном файле.
143
+ ВАЖНО: Эта функция работает только для файлов `.ini` и спроектирована так,
144
+ чтобы сохранять комментарии и структуру исходного файла.
145
+ При работе с `.yml` файлами она вернет `False`.
146
+ """
147
+ path = _get_config_path(cfg_file)
148
+ file_ext = Path(path).suffix.lower()
149
+
150
+ # Защита: работаем только с .ini файлами
151
+ if file_ext != '.ini':
152
+ logger.warning(f"Сохранение поддерживается только для .ini файлов. Файл {path} не будет изменен.")
153
+ return False
154
+
155
+ if not os.path.exists(path):
156
+ logger.error(f"Невозможно сохранить значение: файл конфигурации {path} не найден.")
157
+ return False
158
+
159
+ try:
160
+ with open(path, 'r', encoding='utf-8') as f:
161
+ lines = f.readlines()
162
+ except IOError as e:
163
+ logger.error(f"Ошибка чтения файла {path} для сохранения: {e}")
164
+ return False
165
+
166
+ updated = False
167
+ in_target_section = False
168
+ section_found = False
169
+ key_found_in_section = False
170
+ section_pattern = re.compile(r'^\s*\[\s*(?P<section_name>[^]]+)\s*\]\s*')
171
+ key_pattern = re.compile(rf'^\s*({re.escape(key)})\s*=\s*(.*)', re.IGNORECASE)
172
+
173
+ new_lines = []
174
+ for line in lines:
175
+ section_match = section_pattern.match(line)
176
+ if section_match:
177
+ current_section_name = section_match.group('section_name').strip()
178
+ if current_section_name.lower() == section.lower():
179
+ in_target_section = True
180
+ section_found = True
181
+ else:
182
+ in_target_section = False
183
+ new_lines.append(line)
184
+ continue
185
+
186
+ if in_target_section and not key_found_in_section:
187
+ key_match = key_pattern.match(line)
188
+ if key_match:
189
+ original_key = key_match.group(1)
190
+ new_line_content = f"{original_key} = {value}\n"
191
+ new_lines.append(new_line_content)
192
+ key_found_in_section = True
193
+ updated = True
194
+ logger.info(f"Ключ '{key}' в секции '[{section}]' будет обновлен на '{value}' в файле {path}")
195
+ continue
196
+
197
+ new_lines.append(line)
198
+
199
+ if not section_found:
200
+ logger.warning(f"Секция '[{section}]' не найдена в файле {path}. Значение НЕ сохранено.")
201
+ return False
202
+ if section_found and not key_found_in_section:
203
+ logger.warning(f"Ключ '{key}' не найден в секции '[{section}]' файла {path}. Значение НЕ сохранено.")
204
+ return False
205
+
206
+ if updated:
207
+ try:
208
+ with open(path, 'w', encoding='utf-8') as f:
209
+ f.writelines(new_lines)
210
+ logger.info(f"Файл конфигурации {path} успешно обновлен.")
211
+ return True
212
+ except IOError as e:
213
+ logger.error(f"Ошибка записи в файл {path} при сохранении: {e}")
214
+ return False
215
+ else:
216
+ logger.debug(f"Обновление для ключа '{key}' в секции '[{section}]' не потребовалось.")
217
+ return False
218
+
219
+
220
+ # --- Функции-обертки для удобного получения значений ---
221
+
222
+ def get_config_value(section: str, key: str, fallback: Any = "", config: Optional[Dict] = None) -> Any:
223
+ """Получает значение из конфигурации."""
224
+ if config is None: config = get_config()
225
+ return config.get(section, {}).get(key, fallback)
226
+
227
+
228
+ def get_config_int(section: str, key: str, fallback: int = 0, config: Optional[Dict] = None) -> int:
229
+ """Получает целочисленное значение."""
230
+ value = get_config_value(section, key, fallback, config)
231
+ try:
232
+ return int(value)
233
+ except (ValueError, TypeError):
234
+ return fallback
235
+
236
+
237
+ def get_config_float(section: str, key: str, fallback: float = 0.0, config: Optional[Dict] = None) -> float:
238
+ """Получает дробное значение."""
239
+ value = get_config_value(section, key, fallback, config)
240
+ try:
241
+ return float(value)
242
+ except (ValueError, TypeError):
243
+ return fallback
244
+
245
+
246
+ def get_config_boolean(section: str, key: str, fallback: bool = False, config: Optional[Dict] = None) -> bool:
247
+ """Получает булево значение."""
248
+ value = get_config_value(section, key, fallback, config)
249
+ if isinstance(value, bool):
250
+ return value
251
+ if str(value).lower() in ['true', '1', 't', 'y', 'yes']:
252
+ return True
253
+ if str(value).lower() in ['false', '0', 'f', 'n', 'no']:
254
+ return False
255
+ return fallback
256
+
257
+
258
+ def get_config_list(
259
+ section: str,
260
+ key: str,
261
+ fallback: Optional[List[Any]] = None,
262
+ config: Optional[Dict] = None) -> List[Any]:
263
+ """Получает значение как список."""
264
+ value = get_config_value(section, key, fallback, config)
265
+ if isinstance(value, list):
266
+ return value
267
+ if fallback is None:
268
+ return []
269
+ return fallback
270
+
271
+
272
+ def get_config_section(section_name: str, fallback: Optional[Dict] = None, config: Optional[Dict] = None) -> Dict[
273
+ str,
274
+ Any]:
275
+ """Получает всю секцию как словарь."""
276
+ if config is None: config = get_config()
277
+ return config.get(section_name, fallback if fallback is not None else {})
@@ -0,0 +1,203 @@
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
+ class ChutilsLogger(logging.Logger):
30
+ """
31
+ Кастомный класс логгера, который расширяет стандартный `logging.Logger`.
32
+
33
+ Основная цель этого класса — добавить поддержку пользовательских уровней
34
+ логирования (`devdebug` и `mediumdebug`), обеспечивая при этом
35
+ корректную работу статических анализаторов и автодополнения в IDE.
36
+
37
+ Вам не нужно создавать экземпляр этого класса напрямую. Используйте
38
+ функцию `setup_logger()`, которая автоматически вернет объект этого типа.
39
+
40
+ :Example:
41
+ from chutils.logger import setup_logger, ChutilsLogger
42
+
43
+ # Используем наш класс для аннотации типа, чтобы IDE давала подсказки
44
+ logger: ChutilsLogger = setup_logger()
45
+
46
+ # Теперь IDE знает об этом методе и не будет показывать предупреждений
47
+ logger.mediumdebug("Это сообщение с автодополнением.")
48
+ """
49
+
50
+ def mediumdebug(self, message, *args, **kws):
51
+ """Логирует сообщение с уровнем MEDIUMDEBUG (15)."""
52
+ if self.isEnabledFor(MEDIUMDEBUG_LEVEL_NUM):
53
+ self._log(MEDIUMDEBUG_LEVEL_NUM, message, args, **kws)
54
+
55
+ def devdebug(self, message, *args, **kws):
56
+ """Логирует сообщение с уровнем DEVDEBUG (9)."""
57
+ if self.isEnabledFor(DEVDEBUG_LEVEL_NUM):
58
+ self._log(DEVDEBUG_LEVEL_NUM, message, args, **kws)
59
+
60
+
61
+ logging.setLoggerClass(ChutilsLogger)
62
+
63
+ # --- Глобальное состояние для "ленивой" инициализации ---
64
+
65
+ # Кэш для пути к директории логов. Изначально пуст.
66
+ _LOG_DIR: Optional[str] = None
67
+ # Глобальный экземпляр основного логгера приложения
68
+ _logger_instance: Optional[ChutilsLogger] = None
69
+ # Флаг, чтобы сообщение об инициализации выводилось только один раз
70
+ _initialization_message_shown = False
71
+
72
+
73
+ def _get_log_dir() -> Optional[str]:
74
+ """
75
+ "Лениво" получает и кэширует путь к директории логов.
76
+
77
+ При первом вызове:
78
+ 1. Запускает поиск корня проекта через модуль config.
79
+ 2. Создает директорию 'logs' в корне проекта, если ее нет.
80
+ 3. Кэширует результат.
81
+ При последующих вызовах немедленно возвращает кэшированный путь.
82
+ """
83
+ global _LOG_DIR
84
+ # Если путь уже кэширован, сразу возвращаем его.
85
+ if _LOG_DIR is not None:
86
+ return _LOG_DIR
87
+
88
+ # Запускаем инициализацию в config, если она еще не была выполнена.
89
+ # Это "сердце" автоматического обнаружения.
90
+ config._initialize_paths()
91
+
92
+ # Берем найденный config'ом базовый каталог проекта.
93
+ base_dir = config._BASE_DIR
94
+
95
+ # Если корень проекта не был найден, файловое логирование невозможно.
96
+ if not base_dir:
97
+ print("ПРЕДУПРЕЖДЕНИЕ: Не удалось определить корень проекта, файловое логирование будет отключено.")
98
+ return None
99
+
100
+ # Создаем путь к директории логов и саму директорию, если нужно.
101
+ log_path = os.path.join(base_dir, 'logs')
102
+ if not os.path.exists(log_path):
103
+ try:
104
+ os.makedirs(log_path)
105
+ print(f"INFO: Создана директория для логов: {log_path}")
106
+ except OSError as e:
107
+ # Если не удалось создать директорию, логирование в файл будет невозможно.
108
+ print(f"ОШИБКА: Не удалось создать директорию для логов {log_path}: {e}")
109
+ return None
110
+
111
+ # Кэшируем успешный результат и возвращаем его.
112
+ _LOG_DIR = log_path
113
+ return _LOG_DIR
114
+
115
+
116
+ def setup_logger(name: str = 'app_logger', log_level_str: str = '') -> ChutilsLogger:
117
+ """
118
+ Настраивает и возвращает логгер с нужным именем.
119
+
120
+ - Предотвращает повторную настройку уже существующего логгера.
121
+ - Читает настройки из config.ini (уровень, имя файла и т.д.).
122
+ - Добавляет обработчики для вывода в консоль и в файл с ежедневной ротацией.
123
+
124
+ Args:
125
+ name (str): Имя логгера. 'app_logger' используется для основного логгера приложения.
126
+ log_level_str (str, optional): Явное указание уровня логирования (например, 'DEBUG').
127
+ Если не задан, берется из конфига.
128
+
129
+ Returns:
130
+ logging.Logger: Настроенный экземпляр логгера.
131
+ """
132
+ global _logger_instance, _initialization_message_shown
133
+
134
+ # Если логгер с таким именем уже имеет обработчики, значит он настроен.
135
+ # Просто возвращаем его, чтобы не дублировать вывод.
136
+ existing_logger = logging.getLogger(name)
137
+ if existing_logger.hasHandlers():
138
+ return existing_logger # type: ignore
139
+
140
+ # Если запрашивается основной логгер приложения и он уже есть в кэше.
141
+ if name == 'app_logger' and _logger_instance:
142
+ return _logger_instance
143
+
144
+ # Получаем директорию для логов. Это первая точка, где запускается вся магия поиска путей.
145
+ log_dir = _get_log_dir()
146
+
147
+ # Загружаем конфигурацию для получения настроек логирования.
148
+ cfg = config.get_config()
149
+
150
+ # Определяем уровень логирования
151
+ if not log_level_str:
152
+ log_level_str = config.get_config_value('Logging', 'log_level', 'INFO', cfg)
153
+ log_level = getattr(logging, log_level_str.upper(), logging.INFO)
154
+
155
+ # Получаем остальные настройки из конфига
156
+ log_file_name = config.get_config_value('Logging', 'log_file_name', 'app.log', cfg)
157
+ backup_count = config.get_config_int('Logging', 'log_backup_count', 3, cfg)
158
+
159
+ # Создаем и настраиваем новый экземпляр логгера
160
+ logger = logging.getLogger(name)
161
+ logger.setLevel(log_level)
162
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
163
+
164
+ # 1. Обработчик для вывода в консоль (StreamHandler)
165
+ console_handler = logging.StreamHandler()
166
+ console_handler.setFormatter(formatter)
167
+ logger.addHandler(console_handler)
168
+
169
+ # 2. Обработчик для записи в файл (TimedRotatingFileHandler)
170
+ # Добавляем его, только если директория логов была успешно определена.
171
+ if log_dir and log_file_name:
172
+ log_file_path = os.path.join(log_dir, log_file_name)
173
+ try:
174
+ # Ротация каждый день ('D'), храним backup_count старых файлов
175
+ file_handler = logging.handlers.TimedRotatingFileHandler(
176
+ log_file_path,
177
+ when="D",
178
+ interval=1,
179
+ backupCount=backup_count,
180
+ encoding='utf-8'
181
+ )
182
+ file_handler.setFormatter(formatter)
183
+ logger.addHandler(file_handler)
184
+
185
+ # Выводим информационное сообщение только один раз для всего приложения
186
+ if not _initialization_message_shown:
187
+ logger.debug(
188
+ f"Логирование настроено. Уровень: {log_level_str}. "
189
+ f"Файл: {log_file_path}, ротация: {backup_count} дней."
190
+ )
191
+ _initialization_message_shown = True
192
+ except Exception as e:
193
+ logger.error(f"Не удалось настроить файловый обработчик логов для {log_file_path}: {e}")
194
+ else:
195
+ if not _initialization_message_shown:
196
+ logger.warning("Директория для логов не настроена. Файловое логирование отключено.")
197
+ _initialization_message_shown = True
198
+
199
+ # Кэшируем основной логгер приложения
200
+ if name == 'app_logger':
201
+ _logger_instance = logger
202
+
203
+ return logger # type: ignore
@@ -0,0 +1,138 @@
1
+ import keyring
2
+ from keyring.errors import NoKeyringError, PasswordDeleteError
3
+ from typing import Optional
4
+ from . import logger as logging
5
+
6
+ logger = logging.setup_logger(__name__)
7
+
8
+
9
+ class SecretManager:
10
+ """
11
+ Универсальный менеджер для безопасного хранения и получения секретов
12
+ с использованием системного хранилища (keyring).
13
+
14
+ Изолирует секреты разных приложений по `service_name`.
15
+ """
16
+
17
+ prefix: str = "Chutils_"
18
+
19
+ def __init__(self, service_name: str) -> None:
20
+ """
21
+ Инициализирует менеджер для конкретного сервиса (приложения).
22
+
23
+ :param service_name: Уникальное имя для твоего приложения.
24
+ Например, 'my_super_app' или 'project_alpha_db'.
25
+ """
26
+ if not service_name or not isinstance(service_name, str):
27
+ raise ValueError("service_name должен быть непустой строкой.")
28
+ self.service_name: str = self.prefix + service_name
29
+ logger.devdebug(f"Менеджер секретов инициализирован для сервиса: '{self.service_name}'")
30
+
31
+ def save_secret(self, key: str, value: str) -> bool:
32
+ """
33
+ Сохраняет пару ключ-значение в системном хранилище.
34
+ Если ключ уже существует, его значение будет перезаписано.
35
+
36
+ :param key: Ключ для секрета (например, 'db_password' или 'api_token').
37
+ :param value: Секретное значение, которое нужно сохранить.
38
+ """
39
+ try:
40
+ keyring.set_password(self.service_name, key, value)
41
+ logger.devdebug(f"Секрет для ключа '{key}' успешно сохранен.")
42
+ return True
43
+ except NoKeyringError:
44
+ logger.error("Ошибка: системное хранилище (keyring) не найдено. Секрет не сохранен.")
45
+ return False
46
+ except Exception as e:
47
+ logger.error(f"Произошла непредвиденная ошибка при сохранении секрета: {e}")
48
+ return False
49
+
50
+ def get_secret(self, key: str) -> Optional[str]:
51
+ """
52
+ Получает секретное значение по ключу из системного хранилища.
53
+
54
+ :param key: Ключ, по которому нужно найти секрет.
55
+ :return: Сохраненное значение или None, если ключ не найден.
56
+ """
57
+ try:
58
+ value = keyring.get_password(self.service_name, key)
59
+ if value is None:
60
+ logger.devdebug(f"Секрет для ключа '{key}' не найден.")
61
+ else:
62
+ logger.devdebug(f"Секрет для ключа '{key}' получен.")
63
+ return value
64
+ except NoKeyringError:
65
+ logger.critical("Ошибка: системное хранилище (keyring) не найдено. Невозможно получить секрет.")
66
+ return None
67
+ except Exception as e:
68
+ logger.error(f"Произошла непредвиденная ошибка при получении секрета: {e}")
69
+ return None
70
+
71
+ def delete_secret(self, key: str) -> bool:
72
+ """
73
+ Удаляет пару ключ-значение из системного хранилища.
74
+
75
+ :param key: Ключ, который нужно удалить.
76
+ """
77
+ try:
78
+ # Сначала проверим, есть ли что удалять, для более понятного вывода
79
+ if self.get_secret(key) is None:
80
+ # Сообщение об отсутствии секрета уже будет выведено из get_secret
81
+ return True
82
+
83
+ keyring.delete_password(self.service_name, key)
84
+ logger.devdebug(f"Секрет для ключа '{key}' успешно удален.")
85
+ return True
86
+ except PasswordDeleteError:
87
+ logger.error(f"Ошибка: не удалось удалить секрет для ключа '{key}'.")
88
+ return False
89
+ except NoKeyringError:
90
+ logger.critical("Ошибка: системное хранилище (keyring) не найдено. Невозможно удалить секрет.")
91
+ return False
92
+ except Exception as e:
93
+ logger.error(f"Произошла непредвиденная ошибка при удалении секрета: {e}")
94
+ return False
95
+
96
+ def update_secret(self, key: str, value: str) -> bool:
97
+ """
98
+ Обновляет значение для существующего ключа.
99
+ Это псевдоним для функции save_secret, так как keyring перезаписывает значение.
100
+
101
+ :param key: Ключ для секрета (например, 'db_password' или 'api_token').
102
+ :param value: Секретное значение, которое нужно сохранить.
103
+ """
104
+ logger.devdebug(f"Обновление секрета для ключа '{key}'...")
105
+ return self.save_secret(key, value)
106
+
107
+
108
+ # --- Пример использования ---
109
+ # Этот блок выполнится, только если запустить этот файл напрямую (python secret_manager.py)
110
+ if __name__ == '__main__':
111
+ # 1. Создаем экземпляр менеджера для нашего приложения "my_test_project"
112
+ secrets = SecretManager("my_test_project")
113
+
114
+ # 2. Определяем ключ для пароля от базы данных
115
+ db_password_key = "postgres_password"
116
+
117
+ # 3. Сохраняем пароль
118
+ secrets.save_secret(db_password_key, "MySuperSecretPassword123!")
119
+
120
+ # 4. Получаем его обратно
121
+ retrieved_password = secrets.get_secret(db_password_key)
122
+ if retrieved_password:
123
+ print(f" -> Полученный пароль: {retrieved_password}")
124
+
125
+ # 5. Пробуем получить несуществующий ключ
126
+ secrets.get_secret("non_existent_key")
127
+
128
+ # 6. Обновляем пароль
129
+ secrets.update_secret(db_password_key, "NewPassword456!")
130
+ retrieved_password_after_update = secrets.get_secret(db_password_key)
131
+ if retrieved_password_after_update:
132
+ print(f" -> Пароль после обновления: {retrieved_password_after_update}")
133
+
134
+ # 7. Удаляем пароль
135
+ secrets.delete_secret(db_password_key)
136
+
137
+ # 8. Убеждаемся, что он удален
138
+ secrets.get_secret(db_password_key)