syscheck-cli 0.2.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.
@@ -0,0 +1,115 @@
1
+ """Команда syscheck watch — live-мониторинг (дашборд)."""
2
+ import time
3
+
4
+ import typer
5
+ from rich.live import Live
6
+ from rich.layout import Layout
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+
10
+ from syscheck import providers
11
+ from syscheck.utils import console, bytes_to_human, seconds_to_human, make_bar, get_color_for_percent
12
+
13
+
14
+ def _cpu_panel() -> Panel:
15
+ d = providers.cpu_info()
16
+ colour = get_color_for_percent(d["percent"])
17
+ lines = [f"overall [{colour}]{d['percent']:5.1f}%[/] {make_bar(d['percent'], 18)}"]
18
+ if d["freq_current"]:
19
+ lines.append(f"freq [cyan]{d['freq_current']:.0f} MHz[/]")
20
+ parts = " ".join(f"[{get_color_for_percent(p)}]{p:3.0f}[/]" for p in (d["per_cpu"] or []))
21
+ lines.append(f"cores {parts}")
22
+ return Panel("\n".join(lines), title="cpu", border_style="dim")
23
+
24
+
25
+ def _ram_panel() -> Panel:
26
+ d = providers.ram_info()
27
+ colour = get_color_for_percent(d["percent"])
28
+ lines = [f"usage [{colour}]{d['percent']:5.1f}%[/] {make_bar(d['percent'], 18)}"]
29
+ lines.append(f"used [cyan]{bytes_to_human(d['used'])}[/] / {bytes_to_human(d['total'])}")
30
+ if d["swap_total"] > 0:
31
+ lines.append(f"swap {d['swap_percent']}%")
32
+ return Panel("\n".join(lines), title="ram", border_style="dim")
33
+
34
+
35
+ def _disk_panel() -> Panel:
36
+ d = providers.disk_info()
37
+ lines = []
38
+ for disk in d["disks"]:
39
+ colour = get_color_for_percent(disk["percent"])
40
+ lines.append(
41
+ f"{disk['device'].ljust(6)} [{colour}]{disk['percent']:4.1f}%[/] {make_bar(disk['percent'], 12)} "
42
+ f"{bytes_to_human(disk['free'])} free"
43
+ )
44
+ return Panel("\n".join(lines) or "[dim]нет дисков[/]", title="disk", border_style="dim")
45
+
46
+
47
+ def _net_panel() -> Panel:
48
+ d = providers.net_info()
49
+ lines = []
50
+ if d["io"]:
51
+ lines.append(f"sent [magenta]{bytes_to_human(d['io']['bytes_sent'])}[/]")
52
+ lines.append(f"recv [magenta]{bytes_to_human(d['io']['bytes_recv'])}[/]")
53
+ for i in d["interfaces"]:
54
+ if i["up"] and i["ipv4"]:
55
+ lines.append(f"[green]{i['name']}[/] {i['ipv4'][0]}")
56
+ return Panel("\n".join(lines), title="network", border_style="dim")
57
+
58
+
59
+ def _proc_panel() -> Panel:
60
+ d = providers.processes(sort_by="cpu", limit=8)
61
+ lines = [f"{'pid':<7}{'process':<22}{'cpu':>6}{'ram':>6}"]
62
+ for p in d:
63
+ cpu = p.get("cpu_percent") or 0
64
+ ram = p.get("memory_percent") or 0
65
+ lines.append(
66
+ f"{p['pid']:<7}{(p['name'] or '?')[:20]:<22}"
67
+ f"{get_color_for_percent(cpu)}{cpu:5.1f}%[/]"
68
+ f"{get_color_for_percent(ram)}{ram:5.1f}%[/]"
69
+ )
70
+ return Panel("\n".join(lines), title="processes", border_style="dim")
71
+
72
+
73
+ def _build_layout() -> Layout:
74
+ layout = Layout()
75
+ layout.split_column(Layout(name="header", size=1), Layout(name="body"))
76
+ layout["body"].split_column(Layout(name="top", ratio=1), Layout(name="bottom", ratio=1))
77
+ layout["top"].split_row(Layout(name="cpu", ratio=2), Layout(name="ram", ratio=1))
78
+ layout["bottom"].split_row(
79
+ Layout(name="disk", ratio=1), Layout(name="net", ratio=1),
80
+ Layout(name="procs", ratio=2), Layout(name="sys_info", ratio=1),
81
+ )
82
+ return layout
83
+
84
+
85
+ def watch_cmd(
86
+ interval: int = typer.Option(2, "--interval", "-i", help="Интервал обновления (сек)"),
87
+ ):
88
+ """Live-мониторинг системы (Dashboard)."""
89
+ console.print("[dim]syscheck watch — ctrl+c для выхода[/]")
90
+
91
+ layout = _build_layout()
92
+ header = Panel(
93
+ Text("syscheck watch", justify="center", style="bold"),
94
+ style="dim",
95
+ )
96
+ try:
97
+ with Live(layout, refresh_per_second=1, screen=True):
98
+ providers.cpu_info()
99
+ for _ in range(1200):
100
+ layout["header"].update(header)
101
+ layout["cpu"].update(_cpu_panel())
102
+ layout["ram"].update(_ram_panel())
103
+ layout["disk"].update(_disk_panel())
104
+ layout["net"].update(_net_panel())
105
+ layout["procs"].update(_proc_panel())
106
+ s = providers.system_info()
107
+ lines = [
108
+ f"uptime {seconds_to_human(s['uptime_seconds'])}",
109
+ f"node {s['node']}",
110
+ f"procs {len(providers.processes(sort_by='cpu', limit=1000))}",
111
+ ]
112
+ layout["sys_info"].update(Panel("\n".join(lines), title="system", border_style="dim"))
113
+ time.sleep(interval)
114
+ except KeyboardInterrupt:
115
+ console.print("\n[dim]мониторинг остановлен.[/]")
syscheck/palette.py ADDED
@@ -0,0 +1,30 @@
1
+ """Палитра WinMon и графические примитивы (bar)."""
2
+
3
+ BG = "#0b0d0f"
4
+ PANEL = "#101316"
5
+ BORDER = "#252a2f"
6
+ TEXT = "#e6e9ec"
7
+ MUTED = "#7d858e"
8
+ DIM = "#555d66"
9
+ ACCENT = "#8bd450"
10
+ WARN = "#e4b84c"
11
+ DANGER = "#e05b5b"
12
+ BLUE = "#62a8ff"
13
+ TRACK = "#20252a"
14
+ TERM_BG = "#080a0c"
15
+
16
+
17
+ def color_for(pct: float) -> str:
18
+ """Цвет нагрузки: зелёный/жёлтый/красный по порогам 75/90."""
19
+ if pct >= 90:
20
+ return DANGER
21
+ if pct >= 75:
22
+ return WARN
23
+ return ACCENT
24
+
25
+
26
+ def bar_pct(pct: float, width: int = 26) -> str:
27
+ """Горизонтальный бар █/░ с цветом нагрузки (markup)."""
28
+ w = int(width * min(max(pct, 0), 100) / 100)
29
+ c = color_for(pct)
30
+ return f"[{c}]{'█' * w}[/][{TRACK}]{'░' * (width - w)}[/]"
syscheck/plugin.py ADDED
@@ -0,0 +1,89 @@
1
+ """Система плагинов для syscheck."""
2
+ import importlib
3
+ import importlib.util
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Dict, Optional, Any
7
+
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+ PLUGINS_DIR = Path(__file__).parent / "plugins"
13
+
14
+
15
+ class PluginBase:
16
+ """Базовый класс для плагинов. Наследуй от этого для создания своих команд."""
17
+
18
+ name: str = "unnamed"
19
+ description: str = "No description"
20
+ version: str = "0.1.0"
21
+
22
+ def execute(self, **kwargs) -> Any:
23
+ """Выполняет команду плагина. Переопредели в дочернем классе."""
24
+ raise NotImplementedError
25
+
26
+
27
+ _registry: Dict[str, PluginBase] = {}
28
+
29
+
30
+ def register_plugin(plugin: PluginBase):
31
+ """Регистрирует плагин в системе."""
32
+ _registry[plugin.name] = plugin
33
+
34
+
35
+ def get_plugins() -> Dict[str, PluginBase]:
36
+ """Возвращает все зарегистрированные плагины."""
37
+ _load_plugins_from_dir()
38
+ return dict(_registry)
39
+
40
+
41
+ def get_plugin(name: str) -> Optional[PluginBase]:
42
+ """Возвращает плагин по имени."""
43
+ plugins = get_plugins()
44
+ return plugins.get(name)
45
+
46
+
47
+ def _load_plugins_from_dir():
48
+ """Загружает все плагины из директории plugins/."""
49
+ if not PLUGINS_DIR.exists():
50
+ PLUGINS_DIR.mkdir(parents=True, exist_ok=True)
51
+ return
52
+
53
+ for py_file in PLUGINS_DIR.glob("*.py"):
54
+ if py_file.name.startswith("_"):
55
+ continue
56
+ _load_module(py_file)
57
+
58
+
59
+ def _load_module(module_path: Path):
60
+ """Загружает один модуль-плагин."""
61
+ spec = importlib.util.spec_from_file_location(
62
+ f"syscheck.plugins.{module_path.stem}",
63
+ module_path,
64
+ )
65
+ if spec is None or spec.loader is None:
66
+ return
67
+
68
+ module = importlib.util.module_from_spec(spec)
69
+ sys.modules[spec.name] = module
70
+
71
+ try:
72
+ spec.loader.exec_module(module)
73
+ except Exception as e:
74
+ console.print(f"[red]Plugin load error ({module_path.name}): {e}[/]")
75
+ return
76
+
77
+ # Ищем классы-наследники PluginBase
78
+ for attr_name in dir(module):
79
+ attr = getattr(module, attr_name)
80
+ if (
81
+ isinstance(attr, type)
82
+ and issubclass(attr, PluginBase)
83
+ and attr is not PluginBase
84
+ ):
85
+ try:
86
+ instance = attr()
87
+ register_plugin(instance)
88
+ except Exception as e:
89
+ console.print(f"[red]Plugin init error ({attr_name}): {e}[/]")
@@ -0,0 +1 @@
1
+ """Папка с пользовательскими плагинами."""
@@ -0,0 +1,38 @@
1
+ """Пример плагина для syscheck.
2
+
3
+ Чтобы создать свой плагин:
4
+ 1. Создай .py файл в папке syscheck/plugins/
5
+ 2. Создай класс, наследующий от PluginBase
6
+ 3. Реализуй метод execute()
7
+ 4. Плагин автоматически подхватится при запуске syscheck
8
+
9
+ Пример:
10
+ syscheck plugins --list # покажет все плагины
11
+ syscheck plugins --run example # запустит этот плагин
12
+ """
13
+ from syscheck.plugin import PluginBase, register_plugin
14
+ from syscheck.utils import console, print_value, print_ok
15
+
16
+
17
+ class ExamplePlugin(PluginBase):
18
+ name = "example"
19
+ description = "Пример плагина — показывает информацию о библиотеках"
20
+ version = "0.1.0"
21
+
22
+ def execute(self, **kwargs):
23
+ import psutil
24
+ from importlib.metadata import version as _pkg_version
25
+ import typer
26
+
27
+ console.print("[dim]dependencies[/]")
28
+ print_value("psutil", psutil.__version__)
29
+ print_value("rich", _pkg_version("rich"))
30
+ print_value("typer", _pkg_version("typer"))
31
+
32
+ console.print()
33
+ print_ok("Создай свой плагин в syscheck/plugins/!")
34
+
35
+
36
+ # Автоматическая регистрация при импорте
37
+ register_plugin(ExamplePlugin())
38
+