chutils 2.7.0__tar.gz → 2.7.2__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.7.0 → chutils-2.7.2}/PKG-INFO +1 -1
- {chutils-2.7.0 → chutils-2.7.2}/pyproject.toml +1 -1
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/__init__.py +32 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cli_utils.py +5 -17
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/config.py +13 -2
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/dev.py +12 -2
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/paths.py +4 -4
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/secrets.py +12 -2
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/__init__.py +21 -2
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/core.py +2 -2
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/diagnostics.py +6 -3
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/manager.py +10 -1
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/schema.py +12 -7
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/watcher.py +6 -4
- chutils-2.7.2/src/chutils/env.py +48 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/core.py +5 -28
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/formatters.py +16 -9
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/tracing.py +11 -5
- {chutils-2.7.0 → chutils-2.7.2}/LICENSE +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/README.md +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/__init__.pyi +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/__init__.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/base.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/decorator.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/in_memory.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/utils.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cli.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cli_booster.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/__init__.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/base.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/init.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/template.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/utils.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/validate.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/GEMINI.md +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/generator.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/getters.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/providers.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/utils.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/context.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/decorators.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/dev/__init__.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/dev/ast_indexer.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/dev/models.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/exceptions.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/features.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/fs.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/lifecycle.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/GEMINI.md +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/__init__.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/handlers.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/masking.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/secret_manager/GEMINI.md +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/secret_manager/__init__.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/secret_manager/core.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/secret_manager/providers.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/testing/__init__.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/testing/fixtures.py +0 -0
- {chutils-2.7.0 → chutils-2.7.2}/src/chutils/time.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "chutils"
|
|
3
|
-
version = "2.7.
|
|
3
|
+
version = "2.7.2"
|
|
4
4
|
description = "Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах."
|
|
5
5
|
authors = [{name = "Chu4hel", email = "sergeiivanov636@gmail.com"}]
|
|
6
6
|
license = "MIT"
|
|
@@ -70,6 +70,13 @@ _LAZY_MAPPING = {
|
|
|
70
70
|
'generate_yaml_template': ('.config', 'generate_yaml_template'),
|
|
71
71
|
'generate_env_template': ('.config', 'generate_env_template'),
|
|
72
72
|
'generate_json_schema': ('.config', 'generate_json_schema'),
|
|
73
|
+
'get_base_dir': ('.config', 'get_base_dir'),
|
|
74
|
+
'get_config_file_path': ('.config', 'get_config_file_path'),
|
|
75
|
+
'is_config_loaded': ('.config', 'is_config_loaded'),
|
|
76
|
+
'are_paths_initialized': ('.config', 'are_paths_initialized'),
|
|
77
|
+
'get_config_paths': ('.config', 'get_config_paths'),
|
|
78
|
+
'get_all_config_paths': ('.config', 'get_all_config_paths'),
|
|
79
|
+
'export_schema': ('.config', 'export_schema'),
|
|
73
80
|
|
|
74
81
|
# features
|
|
75
82
|
'is_feature_enabled': ('.features', 'is_feature_enabled'),
|
|
@@ -78,7 +85,31 @@ _LAZY_MAPPING = {
|
|
|
78
85
|
# logger
|
|
79
86
|
'setup_logger': ('.logger', 'setup_logger'),
|
|
80
87
|
'ChutilsLogger': ('.logger', 'ChutilsLogger'),
|
|
88
|
+
'LogLevel': ('.logger', 'LogLevel'),
|
|
89
|
+
'SecretMaskingFilter': ('.logger', 'SecretMaskingFilter'),
|
|
90
|
+
'ChutilsJsonFormatter': ('.logger', 'ChutilsJsonFormatter'),
|
|
81
91
|
'SafeTimedRotatingFileHandler': ('.logger', 'SafeTimedRotatingFileHandler'),
|
|
92
|
+
'CompressingRotatingFileHandler': ('.logger', 'CompressingRotatingFileHandler'),
|
|
93
|
+
'CompressingTimedRotatingFileHandler': ('.logger', 'CompressingTimedRotatingFileHandler'),
|
|
94
|
+
'DEVDEBUG_LEVEL_NUM': ('.logger', 'DEVDEBUG_LEVEL_NUM'),
|
|
95
|
+
'MEDIUMDEBUG_LEVEL_NUM': ('.logger', 'MEDIUMDEBUG_LEVEL_NUM'),
|
|
96
|
+
|
|
97
|
+
# cli_utils
|
|
98
|
+
'get_console': ('.cli_utils', 'get_console'),
|
|
99
|
+
|
|
100
|
+
# env (Discovery)
|
|
101
|
+
'is_rich_enabled': ('.env', 'is_rich_enabled'),
|
|
102
|
+
'is_otel_enabled': ('.env', 'is_otel_enabled'),
|
|
103
|
+
'RICH_AVAILABLE': ('.env', 'RICH_AVAILABLE'),
|
|
104
|
+
'PYDANTIC_AVAILABLE': ('.env', 'PYDANTIC_AVAILABLE'),
|
|
105
|
+
'WATCHDOG_AVAILABLE': ('.env', 'WATCHDOG_AVAILABLE'),
|
|
106
|
+
'JSON_LOGGER_AVAILABLE': ('.env', 'JSON_LOGGER_AVAILABLE'),
|
|
107
|
+
'OTEL_AVAILABLE': ('.env', 'OTEL_AVAILABLE'),
|
|
108
|
+
|
|
109
|
+
# cache
|
|
110
|
+
'cache_with_ttl': ('.cache', 'cache_with_ttl'),
|
|
111
|
+
'BaseCacheBackend': ('.cache', 'BaseCacheBackend'),
|
|
112
|
+
'InMemoryCacheBackend': ('.cache', 'InMemoryCacheBackend'),
|
|
82
113
|
|
|
83
114
|
# context
|
|
84
115
|
'bind_context': ('.context', 'bind_context'),
|
|
@@ -122,6 +153,7 @@ _LAZY_MAPPING = {
|
|
|
122
153
|
'WatcherInitializationError': ('.exceptions', 'WatcherInitializationError'),
|
|
123
154
|
'OptionalDependencyError': ('.exceptions', 'OptionalDependencyError'),
|
|
124
155
|
'ChutilsTimeoutError': ('.exceptions', 'ChutilsTimeoutError'),
|
|
156
|
+
'CacheError': ('.exceptions', 'CacheError'),
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
|
|
@@ -1,26 +1,14 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import re
|
|
3
2
|
import sys
|
|
4
3
|
from typing import Any
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
from .env import RICH_AVAILABLE, is_rich_enabled
|
|
6
|
+
|
|
7
|
+
if RICH_AVAILABLE:
|
|
9
8
|
from rich.console import Console
|
|
10
9
|
from rich.table import Table
|
|
11
10
|
from rich.panel import Panel
|
|
12
11
|
|
|
13
|
-
RICH_AVAILABLE = True
|
|
14
|
-
except ImportError:
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def is_color_enabled() -> bool:
|
|
19
|
-
"""Проверяет, включена ли цветовая индикация."""
|
|
20
|
-
no_color = os.getenv("NO_COLOR", "").lower() in ["true", "1", "yes", "y"]
|
|
21
|
-
ch_no_color = os.getenv("CH_NO_COLOR", "").lower() in ["true", "1", "yes", "y"]
|
|
22
|
-
return not (no_color or ch_no_color)
|
|
23
|
-
|
|
24
12
|
|
|
25
13
|
class FallbackConsole:
|
|
26
14
|
"""
|
|
@@ -79,7 +67,7 @@ def get_console(stderr: bool = False) -> Any:
|
|
|
79
67
|
if stderr:
|
|
80
68
|
if _err_console is not None:
|
|
81
69
|
return _err_console
|
|
82
|
-
if
|
|
70
|
+
if is_rich_enabled():
|
|
83
71
|
_err_console = Console(stderr=True)
|
|
84
72
|
else:
|
|
85
73
|
_err_console = FallbackConsole(stderr=True)
|
|
@@ -88,7 +76,7 @@ def get_console(stderr: bool = False) -> Any:
|
|
|
88
76
|
if _console is not None:
|
|
89
77
|
return _console
|
|
90
78
|
|
|
91
|
-
if
|
|
79
|
+
if is_rich_enabled():
|
|
92
80
|
_console = Console()
|
|
93
81
|
else:
|
|
94
82
|
_console = FallbackConsole()
|
|
@@ -25,7 +25,13 @@ class ConfigCommand(BaseCommand):
|
|
|
25
25
|
debug_parser = config_subparsers.add_parser(
|
|
26
26
|
"debug",
|
|
27
27
|
help="Интерактивный отладчик конфигурации (Trace)",
|
|
28
|
-
description="Показывает итоговую конфигурацию и историю её изменения из разных источников."
|
|
28
|
+
description="Показывает итоговую конфигурацию и историю её изменения из разных источников.",
|
|
29
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
30
|
+
epilog="""Примеры использования:
|
|
31
|
+
chutils config debug
|
|
32
|
+
chutils config debug --format table
|
|
33
|
+
chutils config debug --show-secrets --format json
|
|
34
|
+
"""
|
|
29
35
|
)
|
|
30
36
|
debug_parser.add_argument(
|
|
31
37
|
"-f", "--format",
|
|
@@ -44,7 +50,12 @@ class ConfigCommand(BaseCommand):
|
|
|
44
50
|
schema_parser = config_subparsers.add_parser(
|
|
45
51
|
"generate-schema",
|
|
46
52
|
help="Генерация JSON Schema на основе Pydantic модели",
|
|
47
|
-
description="Создает JSON схему для валидации файлов конфигурации в IDE или AI-агентах."
|
|
53
|
+
description="Создает JSON схему для валидации файлов конфигурации в IDE или AI-агентах.",
|
|
54
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
55
|
+
epilog="""Примеры использования:
|
|
56
|
+
chutils config generate-schema --model my_app.models:Settings -o config.schema.json
|
|
57
|
+
chutils config generate-schema --model chutils.config.schema:TestModel --stdout
|
|
58
|
+
"""
|
|
48
59
|
)
|
|
49
60
|
schema_parser.add_argument(
|
|
50
61
|
"--model",
|
|
@@ -28,7 +28,13 @@ class DevCommand(BaseCommand):
|
|
|
28
28
|
gen_parser = dev_subparsers.add_parser(
|
|
29
29
|
"generate-context",
|
|
30
30
|
help="Сгенерировать карту публичного API (экспорты)",
|
|
31
|
-
description="Сканирует chutils и создает отчет о доступных функциях, классах и декораторах."
|
|
31
|
+
description="Сканирует chutils и создает отчет о доступных функциях, классах и декораторах.",
|
|
32
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
33
|
+
epilog="""Примеры использования:
|
|
34
|
+
chutils dev generate-context -o api_map.md
|
|
35
|
+
chutils dev generate-context --tree -o project_index.json
|
|
36
|
+
chutils dev generate-context -f json --no-weights
|
|
37
|
+
"""
|
|
32
38
|
)
|
|
33
39
|
gen_parser.add_argument(
|
|
34
40
|
"-f", "--format",
|
|
@@ -127,7 +133,11 @@ class DevCommand(BaseCommand):
|
|
|
127
133
|
self.console.print(
|
|
128
134
|
f"[bold green] [OK] [/bold green] Контекст успешно сохранен в: [cyan]{args.output}[/cyan]")
|
|
129
135
|
else:
|
|
130
|
-
|
|
136
|
+
if args.format == "json":
|
|
137
|
+
# В stdout выводим чистый JSON для парсинга ИИ
|
|
138
|
+
print(output_content)
|
|
139
|
+
else:
|
|
140
|
+
self.console.print("\n" + output_content)
|
|
131
141
|
|
|
132
142
|
def _handle_tree_index(self, args: argparse.Namespace):
|
|
133
143
|
"""Генерация иерархического индекса (Phase 5)."""
|
|
@@ -2,6 +2,7 @@ import argparse
|
|
|
2
2
|
import json
|
|
3
3
|
|
|
4
4
|
from chutils import config
|
|
5
|
+
|
|
5
6
|
from .base import BaseCommand
|
|
6
7
|
|
|
7
8
|
|
|
@@ -35,7 +36,7 @@ class ShowPathsCommand(BaseCommand):
|
|
|
35
36
|
config.get_base_dir()
|
|
36
37
|
|
|
37
38
|
base_dir = config.get_base_dir()
|
|
38
|
-
main_path, env_path, local_path = config.
|
|
39
|
+
main_path, env_path, local_path = config.get_all_config_paths()
|
|
39
40
|
|
|
40
41
|
paths_data = {
|
|
41
42
|
"base_dir": base_dir,
|
|
@@ -48,10 +49,9 @@ class ShowPathsCommand(BaseCommand):
|
|
|
48
49
|
if args.json:
|
|
49
50
|
print(json.dumps(paths_data, indent=4, ensure_ascii=False))
|
|
50
51
|
else:
|
|
51
|
-
from chutils.
|
|
52
|
-
import os
|
|
52
|
+
from chutils.env import is_rich_enabled
|
|
53
53
|
|
|
54
|
-
if
|
|
54
|
+
if is_rich_enabled():
|
|
55
55
|
from rich.table import Table
|
|
56
56
|
|
|
57
57
|
table = Table(title="Диагностика путей конфигурации", show_header=True, header_style="bold magenta")
|
|
@@ -27,7 +27,12 @@ class SecretsCommand(BaseCommand):
|
|
|
27
27
|
set_parser = secrets_subparsers.add_parser(
|
|
28
28
|
"set",
|
|
29
29
|
help="Сохранить или обновить секрет",
|
|
30
|
-
description="Сохраняет зашифрованное значение в системное хранилище."
|
|
30
|
+
description="Сохраняет зашифрованное значение в системное хранилище.",
|
|
31
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
32
|
+
epilog="""Примеры использования:
|
|
33
|
+
chutils secrets set DB_PASSWORD "mypassword"
|
|
34
|
+
chutils secrets set STRIPE_KEY "sk_test_..." --service my_app
|
|
35
|
+
"""
|
|
31
36
|
)
|
|
32
37
|
set_parser.add_argument("key", help="Имя ключа (например, DB_PASSWORD)")
|
|
33
38
|
set_parser.add_argument("value", help="Значение секрета")
|
|
@@ -41,7 +46,12 @@ class SecretsCommand(BaseCommand):
|
|
|
41
46
|
delete_parser = secrets_subparsers.add_parser(
|
|
42
47
|
"delete",
|
|
43
48
|
help="Удалить секрет из хранилища",
|
|
44
|
-
description="Навсегда удаляет указанный ключ из системного хранилища."
|
|
49
|
+
description="Навсегда удаляет указанный ключ из системного хранилища.",
|
|
50
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
51
|
+
epilog="""Примеры использования:
|
|
52
|
+
chutils secrets delete DB_PASSWORD
|
|
53
|
+
chutils secrets delete STRIPE_KEY --service my_app
|
|
54
|
+
"""
|
|
45
55
|
)
|
|
46
56
|
delete_parser.add_argument("key", help="Имя ключа для удаления")
|
|
47
57
|
delete_parser.add_argument(
|
|
@@ -73,6 +73,7 @@ __all__ = [
|
|
|
73
73
|
'is_config_loaded',
|
|
74
74
|
'are_paths_initialized',
|
|
75
75
|
'get_config_paths',
|
|
76
|
+
'get_all_config_paths',
|
|
76
77
|
'on_config_change',
|
|
77
78
|
'start_config_watcher',
|
|
78
79
|
'stop_config_watcher',
|
|
@@ -207,7 +208,25 @@ def are_paths_initialized() -> bool:
|
|
|
207
208
|
return _cm.paths_initialized
|
|
208
209
|
|
|
209
210
|
|
|
210
|
-
def get_config_paths(cfg_file: Optional[str] = None) -> Tuple[Optional[str], Optional[str]
|
|
211
|
+
def get_config_paths(cfg_file: Optional[str] = None) -> Tuple[Optional[str], Optional[str]]:
|
|
212
|
+
"""
|
|
213
|
+
Возвращает пути к основному и локальному файлам конфигурации.
|
|
214
|
+
|
|
215
|
+
Legacy API для обратной совместимости. Возвращает кортеж из 2 элементов.
|
|
216
|
+
Для получения всех путей (включая env) используйте get_all_config_paths().
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
cfg_file: Опциональный путь к основному файлу.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Кортеж (путь_к_основному, путь_к_локальному).
|
|
223
|
+
"""
|
|
224
|
+
if not _cm.paths_initialized:
|
|
225
|
+
_cm.initialize_paths(find_project_root)
|
|
226
|
+
return _cm.get_config_paths(cfg_file)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_all_config_paths(cfg_file: Optional[str] = None) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
211
230
|
"""
|
|
212
231
|
Возвращает пути к основному, специфичному для окружения и локальному файлам конфигурации.
|
|
213
232
|
|
|
@@ -219,4 +238,4 @@ def get_config_paths(cfg_file: Optional[str] = None) -> Tuple[Optional[str], Opt
|
|
|
219
238
|
"""
|
|
220
239
|
if not _cm.paths_initialized:
|
|
221
240
|
_cm.initialize_paths(find_project_root)
|
|
222
|
-
return _cm.
|
|
241
|
+
return _cm.get_all_config_paths(cfg_file)
|
|
@@ -70,7 +70,7 @@ def get_config(
|
|
|
70
70
|
|
|
71
71
|
_cm.acquire_file_lock()
|
|
72
72
|
try:
|
|
73
|
-
main_path, env_path, local_path = _cm.
|
|
73
|
+
main_path, env_path, local_path = _cm.get_all_config_paths()
|
|
74
74
|
config_data: Dict = {}
|
|
75
75
|
|
|
76
76
|
def load_from_path(path: str) -> Dict:
|
|
@@ -207,7 +207,7 @@ def save_config_value(
|
|
|
207
207
|
if cfg_file:
|
|
208
208
|
path = cfg_file
|
|
209
209
|
else:
|
|
210
|
-
main_path,
|
|
210
|
+
main_path, _, local_path = _cm.get_all_config_paths()
|
|
211
211
|
if save_to_local and local_path:
|
|
212
212
|
path = local_path
|
|
213
213
|
logger.debug("Для сохранения выбран локальный файл конфигурации: %s", path)
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import json
|
|
6
6
|
from typing import Dict, List, Any
|
|
7
7
|
|
|
8
|
-
from chutils.cli_utils import
|
|
8
|
+
from chutils.cli_utils import get_console
|
|
9
|
+
from chutils.env import is_rich_enabled
|
|
9
10
|
|
|
10
11
|
# Список ключевых слов, значения которых должны маскироваться по умолчанию
|
|
11
12
|
SECRET_KEYWORDS = {
|
|
@@ -59,9 +60,10 @@ def _format_json(trace_data: Dict[str, Dict[str, List[Dict[str, Any]]]], show_se
|
|
|
59
60
|
|
|
60
61
|
def _format_table(trace_data: Dict[str, Dict[str, List[Dict[str, Any]]]], show_secrets: bool) -> str:
|
|
61
62
|
"""Форматирует трассировку в таблицу (Rich или текст)."""
|
|
63
|
+
use_rich = is_rich_enabled()
|
|
62
64
|
console = get_console()
|
|
63
65
|
|
|
64
|
-
if
|
|
66
|
+
if use_rich:
|
|
65
67
|
from rich.table import Table
|
|
66
68
|
table = Table(title="Трассировка конфигурации (Diagnostics)", show_lines=True)
|
|
67
69
|
table.add_column("Секция", style="cyan")
|
|
@@ -100,9 +102,10 @@ def _format_table(trace_data: Dict[str, Dict[str, List[Dict[str, Any]]]], show_s
|
|
|
100
102
|
|
|
101
103
|
def _format_tree(trace_data: Dict[str, Dict[str, List[Dict[str, Any]]]], show_secrets: bool) -> str:
|
|
102
104
|
"""Форматирует трассировку в дерево (Rich или текст)."""
|
|
105
|
+
use_rich = is_rich_enabled()
|
|
103
106
|
console = get_console()
|
|
104
107
|
|
|
105
|
-
if
|
|
108
|
+
if use_rich:
|
|
106
109
|
from rich.tree import Tree
|
|
107
110
|
root = Tree("📁 [bold blue]Configuration Root[/bold blue]")
|
|
108
111
|
|
|
@@ -330,7 +330,16 @@ class _ConfigManager:
|
|
|
330
330
|
|
|
331
331
|
self.paths_initialized = True
|
|
332
332
|
|
|
333
|
-
def get_config_paths(self, cfg_file: Optional[str] = None) -> Tuple[Optional[str], Optional[str]
|
|
333
|
+
def get_config_paths(self, cfg_file: Optional[str] = None) -> Tuple[Optional[str], Optional[str]]:
|
|
334
|
+
"""
|
|
335
|
+
Возвращает пути к основному и локальному файлам конфигурации (Legacy API).
|
|
336
|
+
|
|
337
|
+
Для получения всех путей (включая env) используйте get_all_config_paths().
|
|
338
|
+
"""
|
|
339
|
+
main, _, local = self.get_all_config_paths(cfg_file)
|
|
340
|
+
return main, local
|
|
341
|
+
|
|
342
|
+
def get_all_config_paths(self, cfg_file: Optional[str] = None) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
334
343
|
"""
|
|
335
344
|
Возвращает пути к основному, специфичному для окружения и локальному файлам конфигурации.
|
|
336
345
|
"""
|
|
@@ -7,19 +7,24 @@ import json
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Optional, Type, Union
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
from pydantic import BaseModel
|
|
10
|
+
from ..env import PYDANTIC_AVAILABLE
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if PYDANTIC_AVAILABLE:
|
|
13
|
+
try:
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
except ImportError:
|
|
16
|
+
# Редкий случай рассинхрона
|
|
17
|
+
PYDANTIC_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseModel: # type: ignore
|
|
21
|
+
pass
|
|
22
|
+
else:
|
|
15
23
|
class BaseModel: # type: ignore
|
|
16
24
|
"""Заглушка для работы без Pydantic."""
|
|
17
25
|
pass
|
|
18
26
|
|
|
19
27
|
|
|
20
|
-
PYDANTIC_AVAILABLE = False
|
|
21
|
-
|
|
22
|
-
|
|
23
28
|
def _check_pydantic() -> None:
|
|
24
29
|
"""Проверяет наличие Pydantic."""
|
|
25
30
|
if not PYDANTIC_AVAILABLE:
|
|
@@ -4,8 +4,10 @@ from pathlib import Path
|
|
|
4
4
|
from typing import List, Callable
|
|
5
5
|
|
|
6
6
|
from chutils.exceptions import OptionalDependencyError
|
|
7
|
+
|
|
7
8
|
from .manager import _cm
|
|
8
9
|
from .utils import find_project_root
|
|
10
|
+
from .. import env
|
|
9
11
|
|
|
10
12
|
# Настраиваем логгер
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
@@ -82,15 +84,15 @@ def start_config_watcher() -> bool:
|
|
|
82
84
|
Raises:
|
|
83
85
|
OptionalDependencyError: Если пакет `watchdog` не установлен.
|
|
84
86
|
"""
|
|
85
|
-
|
|
86
|
-
from watchdog.observers import Observer
|
|
87
|
-
except ImportError:
|
|
87
|
+
if not env.WATCHDOG_AVAILABLE:
|
|
88
88
|
raise OptionalDependencyError(
|
|
89
89
|
"Пакет 'watchdog' необходим для работы hot-reload. "
|
|
90
90
|
"Установите его с помощью 'pip install chutils[watch]' или 'poetry add watchdog'.",
|
|
91
91
|
dependency="watchdog"
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
+
from watchdog.observers import Observer
|
|
95
|
+
|
|
94
96
|
if _cm.observer and _cm.observer.is_alive():
|
|
95
97
|
logger.debug("Watcher конфигурации уже запущен.")
|
|
96
98
|
return True
|
|
@@ -98,7 +100,7 @@ def start_config_watcher() -> bool:
|
|
|
98
100
|
if not _cm.paths_initialized:
|
|
99
101
|
_cm.initialize_paths(find_project_root)
|
|
100
102
|
|
|
101
|
-
main_path, env_path, local_path = _cm.
|
|
103
|
+
main_path, env_path, local_path = _cm.get_all_config_paths()
|
|
102
104
|
files_to_watch = []
|
|
103
105
|
if main_path and Path(main_path).exists():
|
|
104
106
|
files_to_watch.append(main_path)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# --- Проверка доступности внешних библиотек (Discovery) ---
|
|
6
|
+
|
|
7
|
+
def _is_installed(package_name: str) -> bool:
|
|
8
|
+
"""Проверяет наличие пакета в системе без его импорта."""
|
|
9
|
+
return importlib.util.find_spec(package_name) is not None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
RICH_AVAILABLE = _is_installed("rich")
|
|
13
|
+
PYDANTIC_AVAILABLE = _is_installed("pydantic")
|
|
14
|
+
WATCHDOG_AVAILABLE = _is_installed("watchdog")
|
|
15
|
+
JSON_LOGGER_AVAILABLE = _is_installed("pythonjsonlogger")
|
|
16
|
+
OTEL_AVAILABLE = _is_installed("opentelemetry.trace")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_rich_enabled() -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Централизованная проверка: доступен ли Rich и разрешен ли он настройками.
|
|
22
|
+
|
|
23
|
+
Учитывает:
|
|
24
|
+
- Наличие установленного пакета rich.
|
|
25
|
+
- Переменные окружения NO_COLOR, CH_NO_COLOR.
|
|
26
|
+
- Специальную переменную CH_NO_RICH (для тестов и headless).
|
|
27
|
+
"""
|
|
28
|
+
if not RICH_AVAILABLE:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
no_color = os.getenv("NO_COLOR", "").lower() in ["true", "1", "yes", "y"]
|
|
32
|
+
ch_no_color = os.getenv("CH_NO_COLOR", "").lower() in ["true", "1", "yes", "y"]
|
|
33
|
+
ch_no_rich = os.getenv("CH_NO_RICH", "").lower() in ["true", "1", "yes", "y"]
|
|
34
|
+
|
|
35
|
+
return not (no_color or ch_no_color or ch_no_rich)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_otel_enabled() -> bool:
|
|
39
|
+
"""Проверяет, доступен ли OpenTelemetry и не отключен ли он.
|
|
40
|
+
|
|
41
|
+
Учитывает:
|
|
42
|
+
- Наличие установленного пакета opentelemetry.
|
|
43
|
+
- Переменную окружения CH_DISABLE_TRACING
|
|
44
|
+
"""
|
|
45
|
+
if not OTEL_AVAILABLE:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
return os.getenv("CH_DISABLE_TRACING", "").lower() not in ["true", "1", "yes", "y"]
|
|
@@ -26,7 +26,7 @@ from .masking import (
|
|
|
26
26
|
_update_mask_re,
|
|
27
27
|
PREDEFINED_PATTERNS
|
|
28
28
|
)
|
|
29
|
-
from .. import config
|
|
29
|
+
from .. import config, env
|
|
30
30
|
from ..context import ContextFilter
|
|
31
31
|
|
|
32
32
|
# --- Глобальное состояние для асинхронного логирования ---
|
|
@@ -50,32 +50,6 @@ def _stop_all_async_loggers():
|
|
|
50
50
|
|
|
51
51
|
atexit.register(_stop_all_async_loggers)
|
|
52
52
|
|
|
53
|
-
# --- Опциональная интеграция с Rich ---
|
|
54
|
-
|
|
55
|
-
RICH_AVAILABLE = False
|
|
56
|
-
try:
|
|
57
|
-
from rich.logging import RichHandler
|
|
58
|
-
from rich.console import Console
|
|
59
|
-
|
|
60
|
-
RICH_AVAILABLE = True
|
|
61
|
-
except ImportError:
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def is_rich_enabled() -> bool:
|
|
66
|
-
"""
|
|
67
|
-
Проверяет, доступен ли Rich и не отключены ли цвета через переменные окружения.
|
|
68
|
-
"""
|
|
69
|
-
if not RICH_AVAILABLE:
|
|
70
|
-
return False
|
|
71
|
-
|
|
72
|
-
# Проверка стандартных переменных отключения цвета
|
|
73
|
-
no_color = os.getenv("NO_COLOR", "").lower() in ["true", "1", "yes", "y"]
|
|
74
|
-
ch_no_color = os.getenv("CH_NO_COLOR", "").lower() in ["true", "1", "yes", "y"]
|
|
75
|
-
|
|
76
|
-
return not (no_color or ch_no_color)
|
|
77
|
-
|
|
78
|
-
|
|
79
53
|
# --- Пользовательские уровни логирования ---
|
|
80
54
|
|
|
81
55
|
DEVDEBUG_LEVEL_NUM = 9
|
|
@@ -404,8 +378,11 @@ def setup_logger(
|
|
|
404
378
|
else:
|
|
405
379
|
formatter = logging.Formatter(log_format)
|
|
406
380
|
|
|
407
|
-
if is_rich_enabled() and not final_json_format:
|
|
381
|
+
if env.is_rich_enabled() and not final_json_format:
|
|
382
|
+
from rich.logging import RichHandler
|
|
383
|
+
from chutils.cli_utils import get_console
|
|
408
384
|
console_handler = RichHandler(
|
|
385
|
+
console=get_console(stderr=True),
|
|
409
386
|
rich_tracebacks=True,
|
|
410
387
|
markup=True,
|
|
411
388
|
tracebacks_show_locals=True,
|
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
Форматтеры для логов.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
from chutils.env import JSON_LOGGER_AVAILABLE
|
|
6
|
+
|
|
7
|
+
if JSON_LOGGER_AVAILABLE:
|
|
6
8
|
try:
|
|
7
|
-
|
|
9
|
+
try:
|
|
10
|
+
from pythonjsonlogger import json as json_mod
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
jsonlogger = json_mod
|
|
13
|
+
except ImportError:
|
|
14
|
+
from pythonjsonlogger import jsonlogger
|
|
10
15
|
except ImportError:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
# Редкий случай, когда spec найден, но импорт не удался
|
|
17
|
+
JSON_LOGGER_AVAILABLE = False
|
|
18
|
+
else:
|
|
19
|
+
ChutilsJsonFormatter = None
|
|
20
|
+
jsonlogger = None
|
|
14
21
|
|
|
22
|
+
if JSON_LOGGER_AVAILABLE:
|
|
15
23
|
class ChutilsJsonFormatter(jsonlogger.JsonFormatter):
|
|
16
24
|
"""
|
|
17
25
|
Кастомный JSON-форматтер, который группирует контекстные данные
|
|
@@ -40,6 +48,5 @@ try:
|
|
|
40
48
|
log_record['trace_id'] = record.trace_id
|
|
41
49
|
if 'span_id' not in log_record and hasattr(record, 'span_id'):
|
|
42
50
|
log_record['span_id'] = record.span_id
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
JSON_LOGGER_AVAILABLE = False
|
|
51
|
+
else:
|
|
52
|
+
ChutilsJsonFormatter = None
|
|
@@ -9,15 +9,21 @@ import functools
|
|
|
9
9
|
import inspect
|
|
10
10
|
from typing import Any, Callable, Optional, TypeVar, cast
|
|
11
11
|
|
|
12
|
+
from .env import OTEL_AVAILABLE
|
|
13
|
+
|
|
12
14
|
T = TypeVar("T", bound=Callable[..., Any])
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
# Совместимость с внутренним кодом
|
|
17
|
+
IS_OTEL_AVAILABLE = OTEL_AVAILABLE
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
if IS_OTEL_AVAILABLE:
|
|
20
|
+
try:
|
|
21
|
+
from opentelemetry import trace as otel_trace
|
|
22
|
+
except ImportError:
|
|
23
|
+
otel_trace = None # type: ignore
|
|
24
|
+
IS_OTEL_AVAILABLE = False
|
|
25
|
+
else:
|
|
19
26
|
otel_trace = None # type: ignore
|
|
20
|
-
IS_OTEL_AVAILABLE = False
|
|
21
27
|
|
|
22
28
|
|
|
23
29
|
def get_tracer(name: str = "chutils") -> Any:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|