nginx-lens 0.4.0__py3-none-any.whl → 0.5.1__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.
- commands/cli.py +6 -0
- commands/completion.py +174 -0
- commands/health.py +41 -6
- commands/logs.py +10 -2
- commands/metrics.py +495 -0
- commands/resolve.py +32 -2
- commands/validate.py +451 -0
- config/__init__.py +4 -0
- config/config_loader.py +200 -0
- {nginx_lens-0.4.0.dist-info → nginx_lens-0.5.1.dist-info}/METADATA +2 -2
- {nginx_lens-0.4.0.dist-info → nginx_lens-0.5.1.dist-info}/RECORD +19 -11
- {nginx_lens-0.4.0.dist-info → nginx_lens-0.5.1.dist-info}/top_level.txt +2 -0
- upstream_checker/checker.py +47 -7
- upstream_checker/dns_cache.py +216 -0
- utils/__init__.py +4 -0
- utils/progress.py +120 -0
- {nginx_lens-0.4.0.dist-info → nginx_lens-0.5.1.dist-info}/WHEEL +0 -0
- {nginx_lens-0.4.0.dist-info → nginx_lens-0.5.1.dist-info}/entry_points.txt +0 -0
- {nginx_lens-0.4.0.dist-info → nginx_lens-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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=
|
|
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=
|
|
19
|
+
commands/health.py,sha256=Q2qGB02dcMFbp247qg5uxkJFWw84rmCEzZozXcy2wtQ,7606
|
|
19
20
|
commands/include.py,sha256=hsheLfoQ3eUx3irAibhC2ndq3ko0VrLda-WGL9JgIlw,2245
|
|
20
|
-
commands/logs.py,sha256=
|
|
21
|
-
commands/
|
|
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.
|
|
36
|
+
nginx_lens-0.5.1.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=
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
nginx_lens-0.
|
|
40
|
-
nginx_lens-0.
|
|
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.1.dist-info/METADATA,sha256=qmc0BzifHWigs76BS7E5g9flKjFGiw5hZ9HAn_9bkuQ,717
|
|
45
|
+
nginx_lens-0.5.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
46
|
+
nginx_lens-0.5.1.dist-info/entry_points.txt,sha256=qEcecjSyLqcJjbIVlNlTpqAhPqDyaujUV5ZcBTAr3po,48
|
|
47
|
+
nginx_lens-0.5.1.dist-info/top_level.txt,sha256=W4rp9juDAaGS642PMW3zfoHyFMj0yTtXKAwlVz87bao,64
|
|
48
|
+
nginx_lens-0.5.1.dist-info/RECORD,,
|
upstream_checker/checker.py
CHANGED
|
@@ -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
|
-
|
|
114
|
+
result = _resolve_with_dns(host, port)
|
|
100
115
|
else:
|
|
101
|
-
|
|
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 = {
|
|
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
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
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|