chutils 2.7.2__tar.gz → 2.7.4__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.2 → chutils-2.7.4}/PKG-INFO +3 -2
  2. {chutils-2.7.2 → chutils-2.7.4}/README.md +2 -1
  3. {chutils-2.7.2 → chutils-2.7.4}/pyproject.toml +1 -1
  4. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cli_utils.py +35 -3
  5. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/core.py +43 -0
  6. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/getters.py +25 -23
  7. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/providers.py +1 -0
  8. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/dev/ast_indexer.py +53 -5
  9. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/dev/models.py +5 -0
  10. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/env.py +4 -1
  11. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/core.py +10 -0
  12. {chutils-2.7.2 → chutils-2.7.4}/LICENSE +0 -0
  13. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/__init__.py +0 -0
  14. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/__init__.pyi +0 -0
  15. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/__init__.py +0 -0
  16. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/base.py +0 -0
  17. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/decorator.py +0 -0
  18. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/in_memory.py +0 -0
  19. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cache/utils.py +0 -0
  20. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cli.py +0 -0
  21. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/cli_booster.py +0 -0
  22. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/__init__.py +0 -0
  23. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/base.py +0 -0
  24. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/config.py +0 -0
  25. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/dev.py +0 -0
  26. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/init.py +0 -0
  27. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/paths.py +0 -0
  28. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/secrets.py +0 -0
  29. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/template.py +0 -0
  30. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/utils.py +0 -0
  31. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/commands/validate.py +0 -0
  32. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/GEMINI.md +0 -0
  33. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/__init__.py +0 -0
  34. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/diagnostics.py +0 -0
  35. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/generator.py +0 -0
  36. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/manager.py +0 -0
  37. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/schema.py +0 -0
  38. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/utils.py +0 -0
  39. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/config/watcher.py +0 -0
  40. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/context.py +0 -0
  41. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/decorators.py +0 -0
  42. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/dev/__init__.py +0 -0
  43. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/exceptions.py +0 -0
  44. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/features.py +0 -0
  45. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/fs.py +0 -0
  46. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/lifecycle.py +0 -0
  47. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/GEMINI.md +0 -0
  48. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/__init__.py +0 -0
  49. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/formatters.py +0 -0
  50. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/handlers.py +0 -0
  51. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/logger/masking.py +0 -0
  52. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/secret_manager/GEMINI.md +0 -0
  53. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/secret_manager/__init__.py +0 -0
  54. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/secret_manager/core.py +0 -0
  55. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/secret_manager/providers.py +0 -0
  56. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/testing/__init__.py +0 -0
  57. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/testing/fixtures.py +0 -0
  58. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/time.py +0 -0
  59. {chutils-2.7.2 → chutils-2.7.4}/src/chutils/tracing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chutils
3
- Version: 2.7.2
3
+ Version: 2.7.4
4
4
  Summary: Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -83,7 +83,8 @@ Every time you start a new project, you have to solve the same tasks:
83
83
  file. It uses **lazy initialization** — no heavy operations until you actually need them.
84
84
  - **⚙️ Flexible Configuration:** Support for `YAML` and `INI` formats. Simple functions for retrieving typed data.
85
85
  - **✍️ Advanced Logger:** The `setup_logger()` function configures logging to the console and rotating files out of the
86
- box. It returns a custom logger with additional debug levels (`devdebug`, `mediumdebug`).
86
+ box. Includes **automatic secret masking** and **smart console width detection** for IDEs (PyCharm, etc.).
87
+ It returns a custom logger with additional debug levels (`devdebug`, `mediumdebug`).
87
88
  - **🔒 Secure Secret Storage:** The `secret_manager` module provides a simple interface for saving and retrieving secrets
88
89
  via the system `keyring`, with a fallback to `.env` files.
89
90
  - **🚀 CLI Booster:** Turn any function into a CLI tool in seconds using the `@cli_command` decorator with automatic type
@@ -31,7 +31,8 @@ Every time you start a new project, you have to solve the same tasks:
31
31
  file. It uses **lazy initialization** — no heavy operations until you actually need them.
32
32
  - **⚙️ Flexible Configuration:** Support for `YAML` and `INI` formats. Simple functions for retrieving typed data.
33
33
  - **✍️ Advanced Logger:** The `setup_logger()` function configures logging to the console and rotating files out of the
34
- box. It returns a custom logger with additional debug levels (`devdebug`, `mediumdebug`).
34
+ box. Includes **automatic secret masking** and **smart console width detection** for IDEs (PyCharm, etc.).
35
+ It returns a custom logger with additional debug levels (`devdebug`, `mediumdebug`).
35
36
  - **🔒 Secure Secret Storage:** The `secret_manager` module provides a simple interface for saving and retrieving secrets
36
37
  via the system `keyring`, with a fallback to `.env` files.
37
38
  - **🚀 CLI Booster:** Turn any function into a CLI tool in seconds using the `@cli_command` decorator with automatic type
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chutils"
3
- version = "2.7.2"
3
+ version = "2.7.4"
4
4
  description = "Набор простых и удобных утилит для Python, который избавляет от рутины при работе с конфигурацией и логированием в новых проектах."
5
5
  authors = [{name = "Chu4hel", email = "sergeiivanov636@gmail.com"}]
6
6
  license = "MIT"
@@ -1,6 +1,8 @@
1
+ import os
1
2
  import re
3
+ import shutil
2
4
  import sys
3
- from typing import Any
5
+ from typing import Any, Optional
4
6
 
5
7
  from .env import RICH_AVAILABLE, is_rich_enabled
6
8
 
@@ -18,6 +20,10 @@ class FallbackConsole:
18
20
  def __init__(self, stderr: bool = False):
19
21
  self._is_stderr = stderr
20
22
 
23
+ @property
24
+ def file(self):
25
+ return sys.stderr if self._is_stderr else sys.stdout
26
+
21
27
  def _strip_markup(self, text: str) -> str:
22
28
  """Удаляет простейшие теги rich типа [bold]."""
23
29
  return re.sub(r"\[/?[\w\s,=#]*\]", "", text)
@@ -56,6 +62,32 @@ class FallbackConsole:
56
62
 
57
63
  _console = None
58
64
  _err_console = None
65
+ _console_width: Optional[int] = None
66
+
67
+
68
+ def set_console_width(width: int):
69
+ """
70
+ Устанавливает ширину консоли и сбрасывает кэшированные экземпляры консолей.
71
+ """
72
+ global _console_width, _console, _err_console
73
+ _console_width = width
74
+ _console = None
75
+ _err_console = None
76
+
77
+
78
+ def _get_default_width() -> Optional[int]:
79
+ """Определяет ширину консоли по умолчанию с учетом IDE."""
80
+ if _console_width is not None:
81
+ return _console_width
82
+
83
+ # Пытаемся определить размер терминала
84
+ width, _ = shutil.get_terminal_size(fallback=(80, 24))
85
+
86
+ # Специфичное поведение для PyCharm (часто ограничивает ширину в 80 символов при запуске логов)
87
+ if os.getenv("PYCHARM_HOSTED") == "1" and width == 80:
88
+ return 140
89
+
90
+ return width
59
91
 
60
92
 
61
93
  def get_console(stderr: bool = False) -> Any:
@@ -68,7 +100,7 @@ def get_console(stderr: bool = False) -> Any:
68
100
  if _err_console is not None:
69
101
  return _err_console
70
102
  if is_rich_enabled():
71
- _err_console = Console(stderr=True)
103
+ _err_console = Console(stderr=True, width=_get_default_width())
72
104
  else:
73
105
  _err_console = FallbackConsole(stderr=True)
74
106
  return _err_console
@@ -77,7 +109,7 @@ def get_console(stderr: bool = False) -> Any:
77
109
  return _console
78
110
 
79
111
  if is_rich_enabled():
80
- _console = Console()
112
+ _console = Console(width=_get_default_width())
81
113
  else:
82
114
  _console = FallbackConsole()
83
115
  return _console
@@ -7,6 +7,7 @@
7
7
 
8
8
  import asyncio
9
9
  import logging
10
+ import os
10
11
  from pathlib import Path
11
12
  from typing import Any, Optional, Dict, TYPE_CHECKING, TypeVar, Type, Union, Tuple
12
13
 
@@ -131,6 +132,48 @@ def get_config(
131
132
  except Exception as e:
132
133
  logger.error("Ошибка загрузки удаленной конфигурации с %s: %s", remote_url, e)
133
134
 
135
+ # 5. Переменные окружения (CH_SECTION_KEY)
136
+ disable_env_override = os.getenv("CH_DISABLE_ENV_OVERRIDE", "").lower() in ("true", "1", "yes", "y")
137
+ if not disable_env_override:
138
+ env_overrides = {}
139
+ for env_key, env_value in os.environ.items():
140
+ if env_key.startswith("CH_") and env_key not in ("CH_ENV", "CH_DISABLE_ENV_OVERRIDE",
141
+ "CH_DISABLE_KEYRING_WARNING"):
142
+ parts = env_key[3:].split('_', 1)
143
+ if len(parts) == 2:
144
+ section, key = parts
145
+ section_lower = section.lower()
146
+ key_lower = key.lower()
147
+
148
+ # Попытка найти существующий регистр секции
149
+ actual_sec = section_lower
150
+ for existing_sec in config_data.keys():
151
+ if existing_sec.lower() == section_lower:
152
+ actual_sec = existing_sec
153
+ break
154
+
155
+ # Попытка найти существующий регистр ключа
156
+ actual_key = key_lower
157
+ if actual_sec in config_data and isinstance(config_data[actual_sec], dict):
158
+ for existing_key in config_data[actual_sec].keys():
159
+ if existing_key.lower() == key_lower:
160
+ actual_key = existing_key
161
+ break
162
+
163
+ if actual_sec not in env_overrides:
164
+ env_overrides[actual_sec] = {}
165
+ env_overrides[actual_sec][actual_key] = env_value
166
+
167
+ # Специфический ключ для secrets
168
+ secrets_env = os.getenv("CH_DISABLE_KEYRING_WARNING")
169
+ if secrets_env is not None:
170
+ if "secrets" not in env_overrides:
171
+ env_overrides["secrets"] = {}
172
+ env_overrides["secrets"]["disable_keyring"] = secrets_env
173
+
174
+ if env_overrides:
175
+ utils.deep_merge(config_data, env_overrides)
176
+
134
177
  # Записываем переменные окружения в трассировку
135
178
  if _cm.tracing_enabled:
136
179
  _cm.trace_env_vars()
@@ -6,7 +6,6 @@
6
6
  """
7
7
 
8
8
  import logging
9
- import os
10
9
  from pathlib import Path
11
10
  from typing import Any, Optional, List, Dict, TYPE_CHECKING, TypeVar, Type, overload, Union
12
11
 
@@ -29,9 +28,7 @@ def get_config_value(section: str, key: str, fallback: Any = None, config: Optio
29
28
  Получает произвольное значение из конфигурации.
30
29
 
31
30
  Если значение не найдено или оно пустое, возвращает `fallback`.
32
- Для ключа `disable_keyring` в секции `secrets` проверяет переменную окружения.
33
-
34
- Также поддерживает универсальное переопределение через переменные окружения
31
+ Поддерживает универсальное переопределение через переменные окружения
35
32
  по шаблону `CH_[SECTION]_[KEY]`, если не установлено `CH_DISABLE_ENV_OVERRIDE=true`.
36
33
 
37
34
  Args:
@@ -43,27 +40,24 @@ def get_config_value(section: str, key: str, fallback: Any = None, config: Optio
43
40
  Returns:
44
41
  Значение из конфигурации или `fallback`.
45
42
  """
46
- # 1. Проверка глобального флага отключения переопределения через ENV
47
- disable_env_override = os.getenv("CH_DISABLE_ENV_OVERRIDE", "").lower() in ("true", "1", "yes", "y")
48
-
49
- if not disable_env_override:
50
- # 2. Проверка универсального переопределения CH_[SECTION]_[KEY]
51
- # Используем верхний регистр для поиска в ENV согласно спецификации
52
- env_key = f"CH_{section.upper()}_{key.upper()}"
53
- env_value = os.getenv(env_key)
54
- if env_value is not None:
55
- return env_value
56
-
57
- # Проверка переменных окружения для специфических ключей (FR3: приоритет над конфигом)
58
- if section == "secrets" and key == "disable_keyring":
59
- env_val = os.getenv("CH_DISABLE_KEYRING_WARNING")
60
- if env_val is not None:
61
- return env_val
62
-
63
43
  if config is None:
64
44
  config = get_config()
65
45
 
66
- value = config.get(section, {}).get(key)
46
+ section_data = config.get(section)
47
+ if section_data is None:
48
+ for k, v in config.items():
49
+ if k.lower() == section.lower():
50
+ section_data = v
51
+ break
52
+ else:
53
+ section_data = {}
54
+
55
+ value = section_data.get(key)
56
+ if value is None:
57
+ for k, v in section_data.items():
58
+ if k.lower() == key.lower():
59
+ value = v
60
+ break
67
61
 
68
62
  # Если значение не найдено или является пустой строкой, возвращаем fallback
69
63
  if value is None or value == '':
@@ -211,7 +205,15 @@ def get_config_section(
211
205
  if config is None:
212
206
  config = get_config()
213
207
 
214
- section_data = config.get(section_name, fallback if fallback is not None else {})
208
+ section_data = config.get(section_name)
209
+ if section_data is None:
210
+ # Case-insensitive fallback
211
+ for k, v in config.items():
212
+ if k.lower() == section_name.lower():
213
+ section_data = v
214
+ break
215
+ else:
216
+ section_data = fallback if fallback is not None else {}
215
217
 
216
218
  if model is not None:
217
219
  if not utils._check_pydantic():
@@ -166,6 +166,7 @@ class IniConfigProvider(ConfigProvider):
166
166
  try:
167
167
  with open(path, 'r', encoding='utf-8') as f:
168
168
  parser = configparser.ConfigParser()
169
+ parser.optionxform = str # Сохраняем регистр ключей
169
170
  parser.read_string(f.read())
170
171
  flat_ini_config = {s: dict(parser.items(s)) for s in parser.sections()}
171
172
  return self._nest_func(flat_ini_config)
@@ -22,6 +22,8 @@ class Indexer:
22
22
  self.project_root = self.root_path
23
23
 
24
24
  self._graph_map: Dict[str, Dict[str, int]] = {} # {source: {target: weight}}
25
+ self._current_imports: Dict[str, str] = {}
26
+ """Карта импортов текущего модуля {asname: full_path}"""
25
27
  self._public_symbols = self._discover_public_api()
26
28
 
27
29
  @property
@@ -162,11 +164,13 @@ class Indexer:
162
164
  )
163
165
 
164
166
  # Анализ зависимостей
167
+ self._current_imports = {}
165
168
  if tree:
166
169
  for item in tree.body:
167
170
  if isinstance(item, ast.Import):
168
171
  for alias in item.names:
169
172
  self._record_dependency(rel_path, alias.name)
173
+ self._current_imports[alias.asname or alias.name] = alias.name
170
174
  elif isinstance(item, ast.ImportFrom):
171
175
  is_relative = item.level > 0
172
176
  base = item.module if item.module else "." * item.level
@@ -177,6 +181,9 @@ class Indexer:
177
181
  self._record_dependency(rel_path, base, force_internal=is_relative)
178
182
  continue
179
183
 
184
+ full_name = f"{base}.{alias.name}" if not is_relative else f"{base}{alias.name}"
185
+ self._current_imports[alias.asname or alias.name] = full_name
186
+
180
187
  if is_relative:
181
188
  # Для относительных импортов в тестах ожидается база ('.', '..')
182
189
  self._record_dependency(rel_path, base, force_internal=True)
@@ -224,6 +231,10 @@ class Indexer:
224
231
  ))
225
232
  return symbols
226
233
 
234
+ def _resolve_base_class(self, base_name: str) -> str:
235
+ """Разрешает имя базового класса в полный путь импорта."""
236
+ return self._current_imports.get(base_name, base_name)
237
+
227
238
  def _build_symbol(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef], sym_type: str) -> Symbol:
228
239
  """Создает объект Symbol из узла AST."""
229
240
  docstring = ast.get_docstring(node) or ""
@@ -248,15 +259,21 @@ class Indexer:
248
259
 
249
260
  # Декораторы
250
261
  for dec in node.decorator_list:
262
+ dec_name = ""
251
263
  if isinstance(dec, ast.Name):
252
- breadcrumbs.decorators.append(dec.id)
264
+ dec_name = dec.id
253
265
  elif isinstance(dec, ast.Attribute) and isinstance(dec.value, ast.Name):
254
- breadcrumbs.decorators.append(f"{dec.value.id}.{dec.attr}")
266
+ dec_name = f"{dec.value.id}.{dec.attr}"
255
267
  elif isinstance(dec, ast.Call):
256
268
  if isinstance(dec.func, ast.Name):
257
- breadcrumbs.decorators.append(dec.func.id)
269
+ dec_name = dec.func.id
258
270
  elif isinstance(dec.func, ast.Attribute) and isinstance(dec.func.value, ast.Name):
259
- breadcrumbs.decorators.append(f"{dec.func.value.id}.{dec.func.attr}")
271
+ dec_name = f"{dec.func.value.id}.{dec.func.attr}"
272
+
273
+ if dec_name:
274
+ breadcrumbs.decorators.append(dec_name)
275
+ if dec_name == "abstractmethod" or dec_name.endswith(".abstractmethod"):
276
+ breadcrumbs.is_abstract = True
260
277
 
261
278
  # Теги из docstring (:tag:)
262
279
  tags = re.findall(r":([\w-]+):", docstring)
@@ -267,7 +284,7 @@ class Indexer:
267
284
  if "heavy" in breadcrumbs.tags:
268
285
  breadcrumbs.is_heavy = True
269
286
 
270
- return Symbol(
287
+ symbol = Symbol(
271
288
  name=node.name,
272
289
  type=sym_type,
273
290
  signature=signature,
@@ -277,3 +294,34 @@ class Indexer:
277
294
  line_number=node.lineno,
278
295
  layer=self._get_layer(node.name, docstring)
279
296
  )
297
+
298
+ if isinstance(node, ast.ClassDef):
299
+ # Извлекаем базы
300
+ for base in node.bases:
301
+ if isinstance(base, ast.Name):
302
+ symbol.bases.append(self._resolve_base_class(base.id))
303
+ elif isinstance(base, ast.Attribute):
304
+ # Случай типа pydantic.BaseModel
305
+ parts = []
306
+ curr = base
307
+ while isinstance(curr, ast.Attribute):
308
+ parts.append(curr.attr)
309
+ curr = curr.value
310
+ if isinstance(curr, ast.Name):
311
+ parts.append(curr.id)
312
+ symbol.bases.append(".".join(reversed(parts)))
313
+
314
+ # Извлекаем методы
315
+ for item in node.body:
316
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
317
+ # Фильтрация: оставляем публичные, защищенные (_) и __init__.
318
+ # Отбрасываем остальные dunder-методы (__dunder__) и приватные (__private).
319
+ name = item.name
320
+ if name.startswith("__") and name != "__init__":
321
+ continue
322
+
323
+ method_symbol = self._build_symbol(item, "method")
324
+
325
+ symbol.children.append(method_symbol)
326
+
327
+ return symbol
@@ -12,6 +12,7 @@ class Breadcrumbs(BaseModel):
12
12
  is_async: bool = False
13
13
  is_thread_safe: bool = False
14
14
  is_heavy: bool = False
15
+ is_abstract: bool = False
15
16
  decorators: List[str] = Field(default_factory=list)
16
17
  tags: List[str] = Field(default_factory=list)
17
18
  custom_metadata: Dict[str, Any] = Field(default_factory=dict)
@@ -29,6 +30,10 @@ class Symbol(BaseModel):
29
30
  docstring: Optional[str] = None
30
31
  breadcrumbs: Breadcrumbs = Field(default_factory=Breadcrumbs)
31
32
  line_number: int = 0
33
+ bases: List[str] = Field(default_factory=list)
34
+ """Базовые классы (для классов)"""
35
+ children: List["Symbol"] = Field(default_factory=list)
36
+ """Вложенные символы (например, методы класса)"""
32
37
 
33
38
 
34
39
  class Node(BaseModel):
@@ -6,7 +6,10 @@ import os
6
6
 
7
7
  def _is_installed(package_name: str) -> bool:
8
8
  """Проверяет наличие пакета в системе без его импорта."""
9
- return importlib.util.find_spec(package_name) is not None
9
+ try:
10
+ return importlib.util.find_spec(package_name) is not None
11
+ except (ImportError, ModuleNotFoundError):
12
+ return False
10
13
 
11
14
 
12
15
  RICH_AVAILABLE = _is_installed("rich")
@@ -264,6 +264,16 @@ def setup_logger(
264
264
 
265
265
  final_logger_settings = {**default_settings, **specific_settings}
266
266
 
267
+ # --- Настройка ширины консоли ---
268
+ cli_settings = cfg.get('CLI', {})
269
+ config_width = cli_settings.get('console_width')
270
+ if config_width is not None:
271
+ try:
272
+ from chutils.cli_utils import set_console_width
273
+ set_console_width(int(config_width))
274
+ except (ValueError, TypeError, ImportError):
275
+ pass
276
+
267
277
  # --- Определение флага асинхронности ---
268
278
  if use_async is not None:
269
279
  final_use_async = use_async
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes