nginx-lens 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,29 +12,37 @@ analyzer/unused.py,sha256=Ixzv0bPsw9IafblVwLiAOgugdg2dGu1MJDtuoqzPZiY,1066
12
12
  analyzer/warnings.py,sha256=zC36QMvegA2eQPvZ-P1eysrX_kXHx5A1MUKHKKNvG5c,5784
13
13
  commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  commands/analyze.py,sha256=UUZpPOpEnPglo94AFklX-cMt6HoHPCafmqfW76CIZVg,8667
15
- commands/cli.py,sha256=brzp6xDDWIrm7ibaoT4x94hgAdBB2DVWniXoK8dRylE,782
15
+ commands/cli.py,sha256=_vpXlQr0Rf5bUN2t_fV2x40eAHU6nyOjAVRr8tC0eNo,1081
16
+ commands/completion.py,sha256=I7g70tM9JDc8PddIOS-tAGiqc3HkmQhFuAzMCMe_Jck,6223
16
17
  commands/diff.py,sha256=mf6xkf_8IKa3R-AiTsWmJDUrxqjGT5gaSAX0u5D0jjY,2097
17
18
  commands/graph.py,sha256=lBh2wCPrhsywxcEbz5UABtNdEepTMLmiIzWJt_Uu1mE,5968
18
- commands/health.py,sha256=e6569IAITNf9Mji_E0GbA18S9neA0yfkSsTAZcnwFoM,5343
19
+ commands/health.py,sha256=Q2qGB02dcMFbp247qg5uxkJFWw84rmCEzZozXcy2wtQ,7606
19
20
  commands/include.py,sha256=hsheLfoQ3eUx3irAibhC2ndq3ko0VrLda-WGL9JgIlw,2245
20
- commands/logs.py,sha256=D6vI9YNPgStZ4weM3bSrUIzK0ncpd9Ku7V7jRBDwtPY,15415
21
- commands/resolve.py,sha256=eeln1_5QAR4FvsrPz1ff9cd_i_9yGlPcF_W1omkTNms,4032
21
+ commands/logs.py,sha256=scs5_AD4w4z1LXyYZvB4IBLd3T-LSV_45hD8KtHrdhc,15763
22
+ commands/metrics.py,sha256=MawshQaCOkdeLxlopuOzDAE-Gyjlbs_u1bIP4EsB10Q,18955
23
+ commands/resolve.py,sha256=hxGiavoESKj_RPfezelh7vsxGSccUFMQI0K4-izdrRk,5868
22
24
  commands/route.py,sha256=4bW4sCY2cVORk-hblnE-BJH3oRL6T9cZuigL-2KCRT4,3174
23
25
  commands/syntax.py,sha256=9sjJXheQ9PRZoFm0sO73pEomzbkeYvbBV265XLU6Krk,3423
24
26
  commands/tree.py,sha256=cxq0vL6V3ah5X4ozPOmWgIH4NJbr2J48TTLNxsjzOL8,2182
27
+ commands/validate.py,sha256=LB_a4RHsaIZxXrm9jUZ1McgLefcP1aZ_bJ_i6z5huNM,22682
28
+ config/__init__.py,sha256=vsP76D53exH8CkpWttJLTgXlW52LFgP_zBnxPKkWAtQ,107
29
+ config/config_loader.py,sha256=fqJEnHojkEMEpAez9ymAHUR1TbD0D_RSFGZsdI7zObQ,6927
25
30
  exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
31
  exporter/csv.py,sha256=_hTIs9CihxP9ewB9FcoN-ERmzUKEQs8hf7U8_heHTO0,2815
27
32
  exporter/graph.py,sha256=WYUrqUgCaK6KihgxAcRHaQn4oMo6b7ybC8yb_36ZIsA,3995
28
33
  exporter/html.py,sha256=uquEM-WvBt2aV9GshgaI3UVhYd8sD0QQ-OmuNtvYUdU,798
29
34
  exporter/json_yaml.py,sha256=XqLOBtrh5Xd7RMeofODU8jaTkaeBEpHMNjz4UF39WrQ,11794
30
35
  exporter/markdown.py,sha256=_0mXQIhurGEZ0dO-eq9DbsuKNrgEDIblgtL3DAgYNo8,724
31
- nginx_lens-0.4.0.dist-info/licenses/LICENSE,sha256=g8QXKdvZZC56rU8E12vIeYF6R4jeTWOsblOnYAda3K4,1073
36
+ nginx_lens-0.5.0.dist-info/licenses/LICENSE,sha256=g8QXKdvZZC56rU8E12vIeYF6R4jeTWOsblOnYAda3K4,1073
32
37
  parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
38
  parser/nginx_parser.py,sha256=Sa9FtGAkvTqNzoehBvgLUWPJHLLIZYWH9ugSHW50X8s,3699
34
39
  upstream_checker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- upstream_checker/checker.py,sha256=i3L6XqUHUH5hcyLq5PXx6wOyjzEL_Z7xYCA3FffFOrU,11257
36
- nginx_lens-0.4.0.dist-info/METADATA,sha256=W4PE6mOQdDbsW0S4RTokPg3h2m9f39Fol0RWV3zxuJo,717
37
- nginx_lens-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
38
- nginx_lens-0.4.0.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
39
- nginx_lens-0.4.0.dist-info/top_level.txt,sha256=mxLJO4rZg0rbixVGhplF3fUNFs8vxDIL25ronZNvRy4,51
40
- nginx_lens-0.4.0.dist-info/RECORD,,
40
+ upstream_checker/checker.py,sha256=b3E7P9f_7JRWqXa_mSei6LKchD9yQoLNuwindnvfWYI,13258
41
+ upstream_checker/dns_cache.py,sha256=RiGgDFKaIvVQz8Rrm8lBorct8WzbyXHLKZy6W7WhYg4,6903
42
+ utils/__init__.py,sha256=tl98tkuTjz9Q5TKD8cxAxBh6n1Yk65TgKCdIbPFsnz4,43
43
+ utils/progress.py,sha256=Aqb1EW7yGJUSSzw5hTJYiKQ3XjU7ABEqAByfQo9t9P0,4797
44
+ nginx_lens-0.5.0.dist-info/METADATA,sha256=oKRRMSyqUAZEhEVSiU5Y_uPqzC9a7kQlOtxxBljzRM4,717
45
+ nginx_lens-0.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
46
+ nginx_lens-0.5.0.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
47
+ nginx_lens-0.5.0.dist-info/top_level.txt,sha256=W4rp9juDAaGS642PMW3zfoHyFMj0yTtXKAwlVz87bao,64
48
+ nginx_lens-0.5.0.dist-info/RECORD,,
@@ -1,5 +1,7 @@
1
1
  analyzer
2
2
  commands
3
+ config
3
4
  exporter
4
5
  parser
5
6
  upstream_checker
7
+ utils
@@ -3,8 +3,9 @@
3
3
  import socket
4
4
  import time
5
5
  import http.client
6
- from typing import Dict, List, Tuple
6
+ from typing import Dict, List, Tuple, Optional
7
7
  from concurrent.futures import ThreadPoolExecutor, as_completed
8
+ from utils.progress import ProgressManager
8
9
  try:
9
10
  import dns.resolver
10
11
  import dns.exception
@@ -12,6 +13,8 @@ try:
12
13
  except ImportError:
13
14
  DNS_AVAILABLE = False
14
15
 
16
+ from upstream_checker.dns_cache import get_cache, is_cache_enabled, disable_cache, enable_cache
17
+
15
18
 
16
19
  def check_tcp(address: str, timeout: float, retries: int) -> bool:
17
20
  """
@@ -56,12 +59,15 @@ def check_http(address: str, timeout: float, retries: int) -> bool:
56
59
  return False
57
60
 
58
61
 
59
- def resolve_address(address: str) -> List[str]:
62
+ def resolve_address(address: str, use_cache: bool = True, cache_ttl: int = 300, cache_dir: Optional[str] = None) -> List[str]:
60
63
  """
61
64
  Резолвит адрес upstream сервера в IP-адреса с информацией о CNAME.
62
65
 
63
66
  Args:
64
67
  address: Адрес в формате "host:port" или "host:port параметры"
68
+ use_cache: Использовать ли кэш (по умолчанию True)
69
+ cache_ttl: Время жизни кэша в секундах (по умолчанию 300)
70
+ cache_dir: Директория для кэша (опционально)
65
71
 
66
72
  Returns:
67
73
  Список строк в формате:
@@ -81,6 +87,7 @@ def resolve_address(address: str) -> List[str]:
81
87
  return []
82
88
  host, port = parts
83
89
 
90
+ # Проверка на IP адрес (не кэшируем IP адреса)
84
91
  try:
85
92
  socket.inet_aton(host)
86
93
  return [host_port]
@@ -95,10 +102,25 @@ def resolve_address(address: str) -> List[str]:
95
102
  except (socket.error, OSError):
96
103
  pass
97
104
 
105
+ # Проверяем кэш перед резолвингом
106
+ if use_cache and is_cache_enabled():
107
+ cache = get_cache(ttl=cache_ttl, cache_dir=cache_dir)
108
+ cached_result = cache.get(host, port)
109
+ if cached_result is not None:
110
+ return cached_result
111
+
112
+ # Выполняем резолвинг
98
113
  if DNS_AVAILABLE:
99
- return _resolve_with_dns(host, port)
114
+ result = _resolve_with_dns(host, port)
100
115
  else:
101
- return _resolve_with_socket(host, port)
116
+ result = _resolve_with_socket(host, port)
117
+
118
+ # Сохраняем в кэш
119
+ if use_cache and is_cache_enabled():
120
+ cache = get_cache(ttl=cache_ttl, cache_dir=cache_dir)
121
+ cache.set(host, port, result)
122
+
123
+ return result
102
124
  except (ValueError, IndexError, AttributeError):
103
125
  return []
104
126
 
@@ -187,7 +209,11 @@ def _resolve_with_socket(host: str, port: str) -> List[str]:
187
209
 
188
210
  def resolve_upstreams(
189
211
  upstreams: Dict[str, List[str]],
190
- max_workers: int = 10
212
+ max_workers: int = 10,
213
+ use_cache: bool = True,
214
+ cache_ttl: int = 300,
215
+ cache_dir: Optional[str] = None,
216
+ progress_manager: Optional[ProgressManager] = None
191
217
  ) -> Dict[str, List[dict]]:
192
218
  """
193
219
  Резолвит DNS имена upstream-серверов в IP-адреса.
@@ -195,6 +221,9 @@ def resolve_upstreams(
195
221
  Args:
196
222
  upstreams: Словарь upstream серверов
197
223
  max_workers: Максимальное количество потоков для параллельной обработки
224
+ use_cache: Использовать ли кэш (по умолчанию True)
225
+ cache_ttl: Время жизни кэша в секундах (по умолчанию 300)
226
+ cache_dir: Директория для кэша (опционально)
198
227
 
199
228
  Возвращает:
200
229
  {
@@ -226,7 +255,10 @@ def resolve_upstreams(
226
255
 
227
256
  # Параллельная обработка резолвинга
228
257
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
229
- future_to_key = {executor.submit(resolve_address, srv): key for key, srv in tasks}
258
+ future_to_key = {
259
+ executor.submit(resolve_address, srv, use_cache, cache_ttl, cache_dir): key
260
+ for key, srv in tasks
261
+ }
230
262
 
231
263
  for future in as_completed(future_to_key):
232
264
  key = future_to_key[future]
@@ -260,7 +292,8 @@ def check_upstreams(
260
292
  timeout: float = 2.0,
261
293
  retries: int = 1,
262
294
  mode: str = "tcp",
263
- max_workers: int = 10
295
+ max_workers: int = 10,
296
+ progress_manager: Optional[ProgressManager] = None
264
297
  ) -> Dict[str, List[dict]]:
265
298
  """
266
299
  Проверяет доступность upstream-серверов.
@@ -305,6 +338,9 @@ def check_upstreams(
305
338
  for key, srv in tasks
306
339
  }
307
340
 
341
+ completed = 0
342
+ total = len(tasks)
343
+
308
344
  for future in as_completed(future_to_key):
309
345
  key = future_to_key[future]
310
346
  name, idx = task_to_key[key]
@@ -313,5 +349,9 @@ def check_upstreams(
313
349
  results[name][idx] = {"address": srv, "healthy": healthy}
314
350
  except Exception:
315
351
  results[name][idx] = {"address": key[2], "healthy": False}
352
+
353
+ completed += 1
354
+ if progress_manager:
355
+ progress_manager.update(completed, total=total, description=f"Проверка upstream серверов ({completed}/{total})")
316
356
 
317
357
  return results
@@ -0,0 +1,216 @@
1
+ """
2
+ Модуль для кэширования результатов DNS резолвинга.
3
+ """
4
+ import os
5
+ import json
6
+ import time
7
+ import hashlib
8
+ from typing import Optional, List, Dict
9
+ from pathlib import Path
10
+
11
+
12
+ class DNSCache:
13
+ """
14
+ Кэш для результатов DNS резолвинга.
15
+ """
16
+
17
+ def __init__(self, cache_dir: Optional[str] = None, ttl: int = 300):
18
+ """
19
+ Инициализация кэша.
20
+
21
+ Args:
22
+ cache_dir: Директория для хранения кэша (по умолчанию ~/.cache/nginx-lens/)
23
+ ttl: Время жизни кэша в секундах (по умолчанию 5 минут)
24
+ """
25
+ self.ttl = ttl
26
+
27
+ if cache_dir:
28
+ self.cache_dir = Path(cache_dir)
29
+ else:
30
+ # Используем ~/.cache/nginx-lens/ или /tmp/nginx-lens-cache/
31
+ home_cache = Path.home() / ".cache" / "nginx-lens"
32
+ tmp_cache = Path("/tmp") / "nginx-lens-cache"
33
+
34
+ # Пробуем использовать домашнюю директорию, если доступна
35
+ try:
36
+ home_cache.mkdir(parents=True, exist_ok=True)
37
+ self.cache_dir = home_cache
38
+ except (OSError, PermissionError):
39
+ # Fallback на /tmp
40
+ try:
41
+ tmp_cache.mkdir(parents=True, exist_ok=True)
42
+ self.cache_dir = tmp_cache
43
+ except (OSError, PermissionError):
44
+ # Последний fallback - текущая директория
45
+ self.cache_dir = Path.cwd() / ".nginx-lens-cache"
46
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
47
+
48
+ self.cache_file = self.cache_dir / "dns_cache.json"
49
+ self._cache: Dict[str, Dict] = {}
50
+ self._load_cache()
51
+
52
+ def _get_cache_key(self, host: str, port: str) -> str:
53
+ """
54
+ Генерирует ключ кэша для host:port.
55
+
56
+ Args:
57
+ host: Имя хоста
58
+ port: Порт
59
+
60
+ Returns:
61
+ Хеш ключ для кэша
62
+ """
63
+ key = f"{host}:{port}"
64
+ return hashlib.md5(key.encode()).hexdigest()
65
+
66
+ def _load_cache(self):
67
+ """Загружает кэш из файла."""
68
+ if self.cache_file.exists():
69
+ try:
70
+ with open(self.cache_file, 'r') as f:
71
+ self._cache = json.load(f)
72
+ except (json.JSONDecodeError, IOError):
73
+ self._cache = {}
74
+
75
+ def _save_cache(self):
76
+ """Сохраняет кэш в файл."""
77
+ try:
78
+ with open(self.cache_file, 'w') as f:
79
+ json.dump(self._cache, f)
80
+ except IOError:
81
+ # Игнорируем ошибки записи
82
+ pass
83
+
84
+ def get(self, host: str, port: str) -> Optional[List[str]]:
85
+ """
86
+ Получает результат из кэша.
87
+
88
+ Args:
89
+ host: Имя хоста
90
+ port: Порт
91
+
92
+ Returns:
93
+ Список резолвленных IP-адресов или None, если нет в кэше или истек TTL
94
+ """
95
+ key = self._get_cache_key(host, port)
96
+
97
+ if key not in self._cache:
98
+ return None
99
+
100
+ cached_data = self._cache[key]
101
+ cached_time = cached_data.get('timestamp', 0)
102
+ current_time = time.time()
103
+
104
+ # Проверяем TTL
105
+ if current_time - cached_time > self.ttl:
106
+ # Удаляем устаревшую запись
107
+ del self._cache[key]
108
+ self._save_cache()
109
+ return None
110
+
111
+ return cached_data.get('result')
112
+
113
+ def set(self, host: str, port: str, result: List[str]):
114
+ """
115
+ Сохраняет результат в кэш.
116
+
117
+ Args:
118
+ host: Имя хоста
119
+ port: Порт
120
+ result: Список резолвленных IP-адресов
121
+ """
122
+ key = self._get_cache_key(host, port)
123
+
124
+ self._cache[key] = {
125
+ 'timestamp': time.time(),
126
+ 'result': result,
127
+ 'host': host,
128
+ 'port': port
129
+ }
130
+
131
+ self._save_cache()
132
+
133
+ def clear(self):
134
+ """Очищает весь кэш."""
135
+ self._cache = {}
136
+ if self.cache_file.exists():
137
+ try:
138
+ self.cache_file.unlink()
139
+ except IOError:
140
+ pass
141
+
142
+ def get_cache_info(self) -> Dict[str, any]:
143
+ """
144
+ Возвращает информацию о кэше.
145
+
146
+ Returns:
147
+ Словарь с информацией о кэше
148
+ """
149
+ current_time = time.time()
150
+ valid_entries = 0
151
+ expired_entries = 0
152
+
153
+ for key, data in self._cache.items():
154
+ cached_time = data.get('timestamp', 0)
155
+ if current_time - cached_time <= self.ttl:
156
+ valid_entries += 1
157
+ else:
158
+ expired_entries += 1
159
+
160
+ return {
161
+ 'total_entries': len(self._cache),
162
+ 'valid_entries': valid_entries,
163
+ 'expired_entries': expired_entries,
164
+ 'cache_dir': str(self.cache_dir),
165
+ 'ttl': self.ttl
166
+ }
167
+
168
+
169
+ # Глобальный экземпляр кэша (будет инициализирован при первом использовании)
170
+ _cache_instance: Optional[DNSCache] = None
171
+ _cache_enabled = True
172
+
173
+
174
+ def get_cache(ttl: int = 300, cache_dir: Optional[str] = None) -> DNSCache:
175
+ """
176
+ Получает глобальный экземпляр кэша.
177
+
178
+ Args:
179
+ ttl: Время жизни кэша в секундах
180
+ cache_dir: Директория для кэша
181
+
182
+ Returns:
183
+ Экземпляр DNSCache
184
+ """
185
+ global _cache_instance
186
+ if _cache_instance is None:
187
+ _cache_instance = DNSCache(cache_dir=cache_dir, ttl=ttl)
188
+ elif ttl != _cache_instance.ttl:
189
+ # Обновляем TTL если изменился
190
+ _cache_instance.ttl = ttl
191
+ return _cache_instance
192
+
193
+
194
+ def clear_cache():
195
+ """Очищает глобальный кэш."""
196
+ global _cache_instance
197
+ if _cache_instance:
198
+ _cache_instance.clear()
199
+
200
+
201
+ def disable_cache():
202
+ """Отключает кэширование."""
203
+ global _cache_enabled
204
+ _cache_enabled = False
205
+
206
+
207
+ def enable_cache():
208
+ """Включает кэширование."""
209
+ global _cache_enabled
210
+ _cache_enabled = True
211
+
212
+
213
+ def is_cache_enabled() -> bool:
214
+ """Проверяет, включено ли кэширование."""
215
+ return _cache_enabled
216
+
utils/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """
2
+ Утилиты для nginx-lens.
3
+ """
4
+
utils/progress.py ADDED
@@ -0,0 +1,120 @@
1
+ """
2
+ Утилиты для отображения прогресса операций.
3
+ """
4
+ from typing import Optional, Callable, Any
5
+ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn, TaskID
6
+ from rich.console import Console
7
+ import signal
8
+ import sys
9
+
10
+ console = Console()
11
+
12
+
13
+ class ProgressManager:
14
+ """
15
+ Менеджер для отображения прогресса операций с поддержкой отмены.
16
+ """
17
+
18
+ def __init__(self, description: str = "Обработка", show_progress: bool = True):
19
+ """
20
+ Инициализация менеджера прогресса.
21
+
22
+ Args:
23
+ description: Описание операции
24
+ show_progress: Показывать ли прогресс-бар
25
+ """
26
+ self.description = description
27
+ self.show_progress = show_progress
28
+ self.progress: Optional[Progress] = None
29
+ self.task_id: Optional[TaskID] = None
30
+ self.cancelled = False
31
+
32
+ # Обработка сигнала прерывания
33
+ self._original_sigint = signal.signal(signal.SIGINT, self._handle_interrupt)
34
+
35
+ def _handle_interrupt(self, signum, frame):
36
+ """Обработка прерывания (Ctrl+C)."""
37
+ self.cancelled = True
38
+ if self.progress:
39
+ self.progress.stop()
40
+ console.print("\n[yellow]Операция отменена пользователем[/yellow]")
41
+ sys.exit(130) # Стандартный exit code для SIGINT
42
+
43
+ def __enter__(self):
44
+ """Вход в контекстный менеджер."""
45
+ if self.show_progress:
46
+ self.progress = Progress(
47
+ SpinnerColumn(),
48
+ TextColumn("[progress.description]{task.description}"),
49
+ BarColumn(),
50
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
51
+ TimeElapsedColumn(),
52
+ TimeRemainingColumn(),
53
+ console=console,
54
+ transient=False,
55
+ )
56
+ self.progress.start()
57
+ self.task_id = self.progress.add_task(self.description, total=None)
58
+ return self
59
+
60
+ def __exit__(self, exc_type, exc_val, exc_tb):
61
+ """Выход из контекстного менеджера."""
62
+ if self.progress:
63
+ self.progress.stop()
64
+ # Восстанавливаем оригинальный обработчик сигнала
65
+ signal.signal(signal.SIGINT, self._original_sigint)
66
+ return False
67
+
68
+ def update(self, completed: int, total: Optional[int] = None, description: Optional[str] = None):
69
+ """
70
+ Обновляет прогресс.
71
+
72
+ Args:
73
+ completed: Количество завершенных задач
74
+ total: Общее количество задач (если None, используется спиннер)
75
+ description: Описание текущей операции
76
+ """
77
+ if self.progress and self.task_id is not None:
78
+ if total is not None:
79
+ self.progress.update(self.task_id, total=total, completed=completed)
80
+ if description:
81
+ self.progress.update(self.task_id, description=description)
82
+
83
+ def advance(self, advance: int = 1):
84
+ """
85
+ Увеличивает прогресс на указанное значение.
86
+
87
+ Args:
88
+ advance: На сколько увеличить прогресс
89
+ """
90
+ if self.progress and self.task_id is not None:
91
+ self.progress.advance(self.task_id, advance)
92
+
93
+
94
+ def with_progress(
95
+ description: str,
96
+ total: Optional[int] = None,
97
+ show_progress: bool = True
98
+ ) -> Callable:
99
+ """
100
+ Декоратор для добавления прогресс-бара к функции.
101
+
102
+ Args:
103
+ description: Описание операции
104
+ total: Общее количество итераций (если известно)
105
+ show_progress: Показывать ли прогресс-бар
106
+
107
+ Returns:
108
+ Декорированная функция
109
+ """
110
+ def decorator(func: Callable) -> Callable:
111
+ def wrapper(*args, **kwargs):
112
+ with ProgressManager(description=description, show_progress=show_progress) as pm:
113
+ # Передаем менеджер прогресса в функцию
114
+ kwargs['progress_manager'] = pm
115
+ if total is not None:
116
+ pm.update(0, total=total)
117
+ return func(*args, **kwargs)
118
+ return wrapper
119
+ return decorator
120
+