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.
Files changed (59) hide show
  1. {chutils-2.7.0 → chutils-2.7.2}/PKG-INFO +1 -1
  2. {chutils-2.7.0 → chutils-2.7.2}/pyproject.toml +1 -1
  3. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/__init__.py +32 -0
  4. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cli_utils.py +5 -17
  5. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/config.py +13 -2
  6. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/dev.py +12 -2
  7. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/paths.py +4 -4
  8. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/secrets.py +12 -2
  9. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/__init__.py +21 -2
  10. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/core.py +2 -2
  11. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/diagnostics.py +6 -3
  12. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/manager.py +10 -1
  13. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/schema.py +12 -7
  14. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/watcher.py +6 -4
  15. chutils-2.7.2/src/chutils/env.py +48 -0
  16. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/core.py +5 -28
  17. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/formatters.py +16 -9
  18. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/tracing.py +11 -5
  19. {chutils-2.7.0 → chutils-2.7.2}/LICENSE +0 -0
  20. {chutils-2.7.0 → chutils-2.7.2}/README.md +0 -0
  21. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/__init__.pyi +0 -0
  22. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/__init__.py +0 -0
  23. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/base.py +0 -0
  24. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/decorator.py +0 -0
  25. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/in_memory.py +0 -0
  26. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cache/utils.py +0 -0
  27. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cli.py +0 -0
  28. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/cli_booster.py +0 -0
  29. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/__init__.py +0 -0
  30. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/base.py +0 -0
  31. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/init.py +0 -0
  32. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/template.py +0 -0
  33. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/utils.py +0 -0
  34. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/commands/validate.py +0 -0
  35. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/GEMINI.md +0 -0
  36. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/generator.py +0 -0
  37. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/getters.py +0 -0
  38. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/providers.py +0 -0
  39. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/config/utils.py +0 -0
  40. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/context.py +0 -0
  41. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/decorators.py +0 -0
  42. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/dev/__init__.py +0 -0
  43. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/dev/ast_indexer.py +0 -0
  44. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/dev/models.py +0 -0
  45. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/exceptions.py +0 -0
  46. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/features.py +0 -0
  47. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/fs.py +0 -0
  48. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/lifecycle.py +0 -0
  49. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/GEMINI.md +0 -0
  50. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/__init__.py +0 -0
  51. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/handlers.py +0 -0
  52. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/logger/masking.py +0 -0
  53. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/secret_manager/GEMINI.md +0 -0
  54. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/secret_manager/__init__.py +0 -0
  55. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/secret_manager/core.py +0 -0
  56. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/secret_manager/providers.py +0 -0
  57. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/testing/__init__.py +0 -0
  58. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/testing/fixtures.py +0 -0
  59. {chutils-2.7.0 → chutils-2.7.2}/src/chutils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chutils
3
- Version: 2.7.0
3
+ Version: 2.7.2
4
4
  Summary: Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chutils"
3
- version = "2.7.0"
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
- # Пытаемся импортировать rich
7
- RICH_AVAILABLE = False
8
- try:
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 RICH_AVAILABLE and is_color_enabled():
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 RICH_AVAILABLE and is_color_enabled():
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
- self.console.print("\n" + output_content)
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.get_config_paths()
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.cli_utils import RICH_AVAILABLE
52
- import os
52
+ from chutils.env import is_rich_enabled
53
53
 
54
- if RICH_AVAILABLE and not os.getenv("CH_NO_RICH"):
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], 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.get_config_paths(cfg_file)
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.get_config_paths()
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, env_path, local_path = _cm.get_config_paths()
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 RICH_AVAILABLE, get_console
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 RICH_AVAILABLE:
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 RICH_AVAILABLE:
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], 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
- try:
11
- from pydantic import BaseModel
10
+ from ..env import PYDANTIC_AVAILABLE
12
11
 
13
- PYDANTIC_AVAILABLE = True
14
- except ImportError:
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
- try:
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.get_config_paths()
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
- try:
5
+ from chutils.env import JSON_LOGGER_AVAILABLE
6
+
7
+ if JSON_LOGGER_AVAILABLE:
6
8
  try:
7
- from pythonjsonlogger import json as json_mod
9
+ try:
10
+ from pythonjsonlogger import json as json_mod
8
11
 
9
- jsonlogger = json_mod
12
+ jsonlogger = json_mod
13
+ except ImportError:
14
+ from pythonjsonlogger import jsonlogger
10
15
  except ImportError:
11
- from pythonjsonlogger import jsonlogger
12
- JSON_LOGGER_AVAILABLE = True
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
- except ImportError:
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
- try:
15
- from opentelemetry import trace as otel_trace
16
+ # Совместимость с внутренним кодом
17
+ IS_OTEL_AVAILABLE = OTEL_AVAILABLE
16
18
 
17
- IS_OTEL_AVAILABLE = True
18
- except ImportError:
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