dockerbrain 1.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,171 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.prompt import Confirm
9
+
10
+ from core.llm import load_llm_config, generate
11
+ from core.optimizer import RuleBasedOptimizer
12
+
13
+ console = Console()
14
+
15
+ _BLOCKED_COMMANDS = {"rm", "rmi", "system prune", "volume rm", "network rm", "image rm"}
16
+
17
+
18
+ def _is_safe_command(command: str) -> bool:
19
+ """Return True if the docker command is NOT destructive."""
20
+ cmd_lower = command.strip().lower()
21
+ if not cmd_lower.startswith("docker"):
22
+ return False
23
+ parts = cmd_lower.split()
24
+ if len(parts) < 2:
25
+ return False
26
+ sub = parts[1]
27
+ if sub in _BLOCKED_COMMANDS:
28
+ return False
29
+ if len(parts) >= 3:
30
+ two_word = f"{parts[1]} {parts[2]}"
31
+ if two_word in _BLOCKED_COMMANDS:
32
+ return False
33
+ return True
34
+
35
+
36
+ _CONTAINER_FIX_SYSTEM_INSTRUCTION = (
37
+ "You are a Docker container optimization expert. Based on the metrics and issues "
38
+ "provided, generate a JSON array of fix actions.\n\n"
39
+ "CRITICAL: Your entire response MUST be a valid JSON array and nothing else. "
40
+ "No markdown, no explanation, no code fences — just the raw JSON.\n\n"
41
+ "Each element in the array must be an object with exactly these keys:\n"
42
+ ' - "container": the container name\n'
43
+ ' - "command": the exact docker CLI command to run (e.g. "docker update --memory 256m my_container")\n'
44
+ ' - "reason": a one-line explanation of why this fix is needed\n\n'
45
+ "Rules:\n"
46
+ "- Only suggest commands that are safe and non-destructive.\n"
47
+ "- Do NOT suggest `docker rm`, `docker rmi`, `docker system prune`, or any delete commands.\n"
48
+ "- Allowed commands include: `docker update`, `docker restart`, `docker stop`, `docker pull`.\n"
49
+ "- Do NOT suggest lowering memory limits just because current usage is low.\n"
50
+ "- If no fixes are needed, return an empty array: []\n"
51
+ )
52
+
53
+
54
+ def fix_containers(container_name: str | None = None) -> None:
55
+ """Analyze containers, get AI fix commands as JSON, confirm and execute each."""
56
+ console.print("[dim]Running analysis...[/]\n")
57
+ optimizer = RuleBasedOptimizer()
58
+ suggestions = optimizer.analyze(container_name=container_name)
59
+
60
+ if not suggestions:
61
+ console.print("[green]No issues found — all containers look healthy![/]")
62
+ return
63
+
64
+ console.print(
65
+ f"[bold]Found {len(suggestions)} issue(s). Preparing fixes...[/]\n"
66
+ )
67
+
68
+ issues_text = "\n".join(
69
+ f"- [{s.severity.value}] {s.container_name} ({s.rule_name}): {s.message}"
70
+ for s in suggestions
71
+ )
72
+
73
+ prompt = (
74
+ "## Container Issues to Fix\n\n"
75
+ f"{issues_text}\n\n"
76
+ "Generate the JSON array of fix actions."
77
+ )
78
+
79
+ config = load_llm_config()
80
+
81
+ with console.status("[bold green]Please wait...[/]", spinner="dots"):
82
+ raw_text = generate(
83
+ prompt=prompt,
84
+ system_instruction=_CONTAINER_FIX_SYSTEM_INSTRUCTION,
85
+ config=config,
86
+ )
87
+
88
+ try:
89
+ actions = json.loads(raw_text)
90
+ except json.JSONDecodeError:
91
+ console.print(
92
+ Panel(
93
+ "[red bold]Could not parse response as JSON.[/]\n\n"
94
+ f"[dim]Raw response:[/]\n{raw_text}",
95
+ title="[bold red]Parse Error[/]",
96
+ border_style="red",
97
+ expand=False,
98
+ )
99
+ )
100
+ return
101
+
102
+ if not actions:
103
+ console.print("[green]No fixes needed.[/]")
104
+ return
105
+
106
+ console.print(
107
+ Panel(
108
+ f"[bold]{len(actions)} fix action(s) proposed:[/]",
109
+ border_style="cyan",
110
+ expand=False,
111
+ )
112
+ )
113
+
114
+ executed = 0
115
+ skipped = 0
116
+ blocked = 0
117
+
118
+ for i, action in enumerate(actions, 1):
119
+ container = action.get("container", "?")
120
+ command = action.get("command", "")
121
+ reason = action.get("reason", "")
122
+
123
+ console.print(f"\n[bold cyan]Fix {i}/{len(actions)}[/]")
124
+ console.print(f" Container: [cyan]{container}[/]")
125
+ console.print(f" Reason: [dim]{reason}[/]")
126
+ console.print(f" Command: [bold]{command}[/]")
127
+
128
+ if not _is_safe_command(command):
129
+ console.print(
130
+ f" [red bold]BLOCKED[/] — This command is destructive and cannot be auto-executed."
131
+ )
132
+ blocked += 1
133
+ continue
134
+
135
+ if Confirm.ask(" Execute?", default=False):
136
+ try:
137
+ result = subprocess.run(
138
+ command.split(),
139
+ capture_output=True,
140
+ text=True,
141
+ timeout=30,
142
+ )
143
+ if result.returncode == 0:
144
+ out = result.stdout.strip()
145
+ console.print(f" [green bold]Done[/]{': ' + out if out else ''}")
146
+ executed += 1
147
+ else:
148
+ console.print(f" [red bold]Failed[/]: {result.stderr.strip()}")
149
+ except Exception as exc:
150
+ console.print(f" [red bold]Error[/]: {exc}")
151
+ else:
152
+ console.print(" [dim]Skipped.[/]")
153
+ skipped += 1
154
+
155
+ console.print()
156
+ summary_parts = []
157
+ if executed:
158
+ summary_parts.append(f"[green]{executed} applied[/]")
159
+ if skipped:
160
+ summary_parts.append(f"[yellow]{skipped} skipped[/]")
161
+ if blocked:
162
+ summary_parts.append(f"[red]{blocked} blocked[/]")
163
+
164
+ console.print(
165
+ Panel(
166
+ " | ".join(summary_parts),
167
+ title="[bold]Fix Summary[/]",
168
+ border_style="cyan",
169
+ expand=False,
170
+ )
171
+ )
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ import json
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from rich.columns import Columns
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.prompt import Confirm
12
+ from rich.syntax import Syntax
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+
16
+ from core.llm import load_llm_config, generate, LLMConfig
17
+
18
+ console = Console()
19
+
20
+ _DEFAULT_DOCKERIGNORE = """\
21
+ .git
22
+ .gitignore
23
+ .dockerignore
24
+ node_modules
25
+ __pycache__
26
+ *.pyc
27
+ .env
28
+ .venv
29
+ *.md
30
+ .DS_Store
31
+ Thumbs.db
32
+ """
33
+
34
+ _DOCKERFILE_DETECT_SYSTEM = (
35
+ "You are a Dockerfile security and optimization expert. Analyze the Dockerfile and return ONLY a "
36
+ "valid JSON array of issues. No markdown, no explanation, no code fences — just the raw JSON.\n\n"
37
+ "Each issue object must have exactly these keys:\n"
38
+ ' - "line": integer (1-indexed) or null if not line-specific\n'
39
+ ' - "rule": short snake_case rule name (e.g. "hardcoded-secret", "cache-busting-copy")\n'
40
+ ' - "severity": one of "HIGH", "MEDIUM", or "LOW"\n'
41
+ ' - "message": a one-sentence explanation of the issue\n\n'
42
+ "Detect all of these if present:\n"
43
+ " - Hardcoded secrets or tokens in ENV instructions\n"
44
+ " - Large or bloated base images (ubuntu, debian, centos — suggest slim/alpine)\n"
45
+ " - apt-get/apk install without --no-install-recommends\n"
46
+ " - Multiple separate RUN apt-get/apk commands (should be chained with &&)\n"
47
+ " - COPY . . before dependency install step (cache-busting)\n"
48
+ " - Missing non-root USER directive\n"
49
+ " - Shell form CMD/ENTRYPOINT instead of exec form (JSON array)\n"
50
+ " - No HEALTHCHECK defined\n"
51
+ " - Packages installed but cache not cleaned (no rm -rf /var/lib/apt/lists/*)\n"
52
+ " - Any other security or optimization issues\n\n"
53
+ "If no issues are found, return an empty JSON array: []"
54
+ )
55
+
56
+ _DOCKERFILE_REWRITE_SYSTEM = (
57
+ "You are a Dockerfile optimization expert. You will be given a Dockerfile and a list of detected issues. "
58
+ "Rewrite the Dockerfile to fix ALL the issues. "
59
+ "Return ONLY the improved Dockerfile content with brief comments explaining changes. "
60
+ "IMPORTANT: NEVER place comments on the same line as code (no inline comments). "
61
+ "Always put comments on a separate line ABOVE the instruction they describe. "
62
+ "Do NOT wrap in markdown code fences. Do NOT add any explanation outside the Dockerfile."
63
+ )
64
+
65
+
66
+ def _ai_detect_dockerfile_issues(content: str, config: LLMConfig) -> list[dict]:
67
+ """Call LLM to detect issues in a Dockerfile. Returns a list of issue dicts."""
68
+
69
+ with console.status("[bold green]Analyzing Dockerfile...[/]", spinner="dots"):
70
+ raw = generate(
71
+ prompt=f"## Dockerfile to Analyze\n\n```dockerfile\n{content}\n```",
72
+ system_instruction=_DOCKERFILE_DETECT_SYSTEM,
73
+ config=config,
74
+ )
75
+
76
+ try:
77
+ return json.loads(raw)
78
+ except json.JSONDecodeError:
79
+ console.print(f"[red]Could not parse AI detection response as JSON.[/]\n[dim]{raw}[/]")
80
+ return []
81
+
82
+
83
+ def _display_ai_issues_table(issues: list[dict], filename: str) -> None:
84
+ """Render AI-detected issues as a Rich table."""
85
+
86
+ _SEV_STYLE = {"HIGH": "bold red", "MEDIUM": "bold yellow", "LOW": "bold green"}
87
+ _SEV_RANK = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
88
+ sorted_issues = sorted(issues, key=lambda i: _SEV_RANK.get(i.get("severity", "LOW"), 9))
89
+
90
+ table = Table(
91
+ title=f"Dockerfile Issues — {filename}",
92
+ expand=True,
93
+ border_style="dim",
94
+ show_lines=True,
95
+ )
96
+ table.add_column("#", justify="right", style="dim", width=4)
97
+ table.add_column("Line", justify="right", width=5)
98
+ table.add_column("Rule", style="bold", no_wrap=True)
99
+ table.add_column("Severity", justify="center", width=8)
100
+ table.add_column("Message")
101
+
102
+ for i, issue in enumerate(sorted_issues, 1):
103
+ sev = issue.get("severity", "LOW")
104
+ line_val = str(issue.get("line")) if issue.get("line") else "—"
105
+ table.add_row(
106
+ str(i),
107
+ line_val,
108
+ issue.get("rule", "unknown"),
109
+ Text(sev, style=_SEV_STYLE.get(sev, "dim")),
110
+ issue.get("message", ""),
111
+ )
112
+
113
+ console.print()
114
+ console.print(table)
115
+ console.print(f"\n[dim]{len(issues)} issue(s) detected.[/]\n")
116
+
117
+
118
+ def _ai_rewrite_dockerfile(content: str, issues: list[dict], config: LLMConfig) -> str:
119
+ """Call LLM to rewrite the Dockerfile fixing all detected issues."""
120
+
121
+ issues_text = "\n".join(
122
+ f"- [{i.get('severity')}] Line {i.get('line') or '?'} ({i.get('rule')}): {i.get('message')}"
123
+ for i in issues
124
+ )
125
+ prompt = (
126
+ f"## Dockerfile\n\n```dockerfile\n{content}\n```\n\n"
127
+ f"## Detected Issues\n\n{issues_text}\n\n"
128
+ "Please rewrite the Dockerfile to fix all the above issues."
129
+ )
130
+
131
+ with console.status("[bold green]Generating fix...[/]", spinner="dots"):
132
+ return generate(
133
+ prompt=prompt,
134
+ system_instruction=_DOCKERFILE_REWRITE_SYSTEM,
135
+ config=config,
136
+ )
137
+
138
+
139
+ def _show_diff(original: str, optimized: str, original_path: Path) -> None:
140
+ """Display a side-by-side Rich diff between original and optimized Dockerfile."""
141
+
142
+ diff = list(difflib.unified_diff(
143
+ original.splitlines(),
144
+ optimized.splitlines(),
145
+ fromfile="Original",
146
+ tofile="Optimized",
147
+ lineterm="",
148
+ ))
149
+ if not diff:
150
+ console.print("[green]The Dockerfile is already optimal.[/]")
151
+ return
152
+
153
+ left = Panel(
154
+ Syntax(original, "dockerfile", theme="monokai", line_numbers=True, word_wrap=True),
155
+ title="[bold red]Original[/]",
156
+ border_style="red",
157
+ expand=True,
158
+ )
159
+ right = Panel(
160
+ Syntax(optimized, "dockerfile", theme="monokai", line_numbers=True, word_wrap=True),
161
+ title="[bold green]Optimized[/]",
162
+ border_style="green",
163
+ expand=True,
164
+ )
165
+ console.print()
166
+ console.print(Columns([left, right], equal=True, expand=True))
167
+
168
+
169
+ def fix_dockerfile(dockerfile_path: str) -> None:
170
+ """AI-powered detect + fix: detects issues, displays them, then rewrites the Dockerfile."""
171
+ original_path = Path(dockerfile_path)
172
+ content = original_path.read_text(encoding="utf-8")
173
+ filename = original_path.name
174
+
175
+ config = load_llm_config()
176
+ issues = _ai_detect_dockerfile_issues(content, config)
177
+
178
+ if not issues:
179
+ console.print("[green bold]No issues detected![/]")
180
+ return
181
+
182
+ _display_ai_issues_table(issues, filename)
183
+ dockerignore_issues = [i for i in issues if i.get("rule") == "missing-dockerignore"]
184
+ dockerfile_issues = [i for i in issues if i.get("rule") != "missing-dockerignore"]
185
+
186
+ if dockerignore_issues:
187
+ dockerignore_path = original_path.parent / ".dockerignore"
188
+ if not dockerignore_path.exists():
189
+ if Confirm.ask(
190
+ f"[bold yellow]Create [cyan]{dockerignore_path}[/cyan]?[/] (fixes missing-dockerignore)",
191
+ default=True,
192
+ ):
193
+ dockerignore_path.write_text(_DEFAULT_DOCKERIGNORE, encoding="utf-8")
194
+ console.print(f"[green bold]Created:[/] [cyan]{dockerignore_path}[/]\n")
195
+ else:
196
+ console.print("[dim]Skipped .dockerignore creation.[/]\n")
197
+
198
+ if not dockerfile_issues:
199
+ console.print("[green]All issues resolved![/]")
200
+ return
201
+
202
+ optimized = _ai_rewrite_dockerfile(content, dockerfile_issues, config)
203
+ _show_diff(content, optimized, original_path)
204
+
205
+ console.print()
206
+ if not Confirm.ask(
207
+ "[bold yellow]Apply this fix?[/] (overwrites the Dockerfile, a .bak backup will be created)",
208
+ default=False,
209
+ ):
210
+ console.print("[dim]Skipped. No changes made.[/]")
211
+ return
212
+
213
+ backup_path = original_path.with_suffix(original_path.suffix + ".bak")
214
+ shutil.copy2(original_path, backup_path)
215
+ console.print(f"[dim]Backup saved to: {backup_path}[/]")
216
+ original_path.write_text(optimized, encoding="utf-8")
217
+ console.print(f"[green bold]Dockerfile fixed![/] Written to: [cyan]{original_path}[/]")
218
+
219
+ console.print("\n[dim]Re-checking for remaining issues...[/]")
220
+ remaining = _ai_detect_dockerfile_issues(optimized, config)
221
+ if not remaining:
222
+ console.print("[green bold]All issues resolved![/]")
223
+ else:
224
+ console.print(f"[yellow]{len(remaining)} issue(s) still remain after fix:[/]")
225
+ _display_ai_issues_table(remaining, filename)
core/llm.py ADDED
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+
9
+ console = Console()
10
+
11
+ _PROVIDER_DEFAULTS: dict[str, dict[str, str]] = {
12
+ "gemini": {
13
+ "model": "gemini-3.1-flash-lite-preview",
14
+ "base_url": "", # uses google-genai SDK, not OpenAI
15
+ },
16
+ "groq": {
17
+ "model": "llama-3.3-70b-versatile",
18
+ "base_url": "https://api.groq.com/openai/v1",
19
+ },
20
+ "ollama": {
21
+ "model": "llama3.1",
22
+ "base_url": "http://localhost:11434/v1",
23
+ },
24
+ }
25
+
26
+ _VALID_PROVIDERS = set(_PROVIDER_DEFAULTS.keys())
27
+
28
+ @dataclass
29
+ class LLMConfig:
30
+ """Resolved LLM configuration."""
31
+
32
+ provider: str
33
+ model: str
34
+ api_key: str
35
+ base_url: str
36
+
37
+
38
+ def _read_rc_section(section: str = "llm") -> dict[str, str]:
39
+ """Read a section from .dockerbrainrc (simple TOML-like INI parser)."""
40
+ rc_path = Path(".dockerbrainrc")
41
+ if not rc_path.exists():
42
+ return {}
43
+
44
+ data: dict[str, str] = {}
45
+ in_section = False
46
+
47
+ for line in rc_path.read_text(encoding="utf-8").splitlines():
48
+ stripped = line.strip()
49
+ if not stripped or stripped.startswith("#"):
50
+ continue
51
+ if stripped.startswith("["):
52
+ in_section = stripped.strip("[] ").lower() == section
53
+ continue
54
+ if in_section and "=" in stripped:
55
+ key, _, val = stripped.partition("=")
56
+ val = val.split("#")[0].strip().strip('"').strip("'")
57
+ data[key.strip()] = val
58
+
59
+ return data
60
+
61
+
62
+ def load_llm_config() -> LLMConfig:
63
+ """Resolve LLM configuration from .dockerbrainrc only."""
64
+
65
+ rc = _read_rc_section("llm")
66
+ provider = (rc.get("provider") or "gemini").lower().strip()
67
+
68
+ if provider not in _VALID_PROVIDERS:
69
+ console.print(
70
+ f"[red bold]Unknown LLM provider:[/] [cyan]{provider}[/]\n"
71
+ f"[dim]Valid providers: {', '.join(sorted(_VALID_PROVIDERS))}[/]"
72
+ )
73
+ raise SystemExit(1)
74
+
75
+ defaults = _PROVIDER_DEFAULTS[provider]
76
+ model = rc.get("model") or defaults["model"]
77
+ base_url = rc.get("base_url") or defaults["base_url"]
78
+ api_key = rc.get("api_key") or ""
79
+
80
+ if not api_key and provider != "ollama":
81
+ _show_missing_key_error(provider)
82
+ raise SystemExit(1)
83
+
84
+ if provider == "ollama" and not api_key:
85
+ api_key = "ollama"
86
+
87
+ return LLMConfig(
88
+ provider=provider,
89
+ model=model,
90
+ api_key=api_key,
91
+ base_url=base_url,
92
+ )
93
+
94
+
95
+ def _show_missing_key_error(provider: str) -> None:
96
+ """Show a brief error for missing API key."""
97
+ console.print(
98
+ Panel(
99
+ f"Add your key to [cyan].dockerbrainrc[/]:\n"
100
+ f' [cyan]api_key = "your_key_here"[/]',
101
+ title="[bold red]Missing API Key[/]",
102
+ border_style="red",
103
+ expand=False,
104
+ )
105
+ )
106
+
107
+ def _strip_code_fences(text: str) -> str:
108
+ """Strip markdown code fences that LLMs sometimes wrap responses in."""
109
+ text = text.strip()
110
+ if text.startswith("```"):
111
+ text = text[text.index("\n") + 1:] if "\n" in text else text
112
+ if text.endswith("```"):
113
+ text = text[:-3].rstrip()
114
+ return text
115
+
116
+
117
+ def generate(
118
+ prompt: str,
119
+ system_instruction: str,
120
+ config: LLMConfig | None = None,
121
+ ) -> str:
122
+ """Send a prompt to the configured LLM and return the response text.
123
+
124
+ Works with any provider: Gemini uses google-genai SDK,
125
+ all others use the OpenAI-compatible API.
126
+ """
127
+ if config is None:
128
+ config = load_llm_config()
129
+
130
+ if config.provider == "gemini":
131
+ return _generate_gemini(prompt, system_instruction, config)
132
+ else:
133
+ return _generate_openai_compat(prompt, system_instruction, config)
134
+
135
+
136
+ def generate_stream(
137
+ prompt: str,
138
+ system_instruction: str,
139
+ config: LLMConfig | None = None,
140
+ ):
141
+ """Yield response chunks from the configured LLM (streaming)."""
142
+ if config is None:
143
+ config = load_llm_config()
144
+
145
+ if config.provider == "gemini":
146
+ yield from _stream_gemini(prompt, system_instruction, config)
147
+ else:
148
+ yield from _stream_openai_compat(prompt, system_instruction, config)
149
+
150
+
151
+ # Gemini backend
152
+ def _generate_gemini(prompt: str, system_instruction: str, config: LLMConfig) -> str:
153
+ from google import genai
154
+ from google.genai import types
155
+
156
+ client = genai.Client(api_key=config.api_key)
157
+ response = client.models.generate_content(
158
+ model=config.model,
159
+ contents=prompt,
160
+ config=types.GenerateContentConfig(
161
+ system_instruction=system_instruction,
162
+ ),
163
+ )
164
+ return _strip_code_fences(response.text)
165
+
166
+ def _stream_gemini(prompt: str, system_instruction: str, config: LLMConfig):
167
+ from google import genai
168
+ from google.genai import types
169
+
170
+ client = genai.Client(api_key=config.api_key)
171
+ stream = client.models.generate_content_stream(
172
+ model=config.model,
173
+ contents=prompt,
174
+ config=types.GenerateContentConfig(
175
+ system_instruction=system_instruction,
176
+ ),
177
+ )
178
+ for chunk in stream:
179
+ if chunk.text:
180
+ yield chunk.text
181
+
182
+
183
+ # OpenAI-compatible backend
184
+ def _generate_openai_compat(prompt: str, system_instruction: str, config: LLMConfig) -> str:
185
+ from openai import OpenAI
186
+
187
+ client = OpenAI(api_key=config.api_key, base_url=config.base_url)
188
+ response = client.chat.completions.create(
189
+ model=config.model,
190
+ messages=[
191
+ {"role": "system", "content": system_instruction},
192
+ {"role": "user", "content": prompt},
193
+ ],
194
+ )
195
+ return _strip_code_fences(response.choices[0].message.content or "")
196
+
197
+ def _stream_openai_compat(prompt: str, system_instruction: str, config: LLMConfig):
198
+ from openai import OpenAI
199
+
200
+ client = OpenAI(api_key=config.api_key, base_url=config.base_url)
201
+ stream = client.chat.completions.create(
202
+ model=config.model,
203
+ messages=[
204
+ {"role": "system", "content": system_instruction},
205
+ {"role": "user", "content": prompt},
206
+ ],
207
+ stream=True,
208
+ )
209
+ for chunk in stream:
210
+ delta = chunk.choices[0].delta if chunk.choices else None
211
+ if delta and delta.content:
212
+ yield delta.content
@@ -0,0 +1,33 @@
1
+ from core.monitor.snapshot import (
2
+ ContainerSnapshot,
3
+ IDLE_CPU_THRESHOLD,
4
+ IDLE_CONSECUTIVE_POLLS,
5
+ MEM_WARNING_PCT,
6
+ MEM_CRITICAL_PCT,
7
+ )
8
+ from core.monitor.display import (
9
+ _format_uptime,
10
+ _make_bar,
11
+ _cpu_color,
12
+ _mem_color,
13
+ build_monitor_layout,
14
+ )
15
+ from core.monitor.collector import (
16
+ ContainerMonitor,
17
+ run_monitor,
18
+ )
19
+
20
+ __all__ = [
21
+ "ContainerSnapshot",
22
+ "ContainerMonitor",
23
+ "run_monitor",
24
+ "build_monitor_layout",
25
+ "IDLE_CPU_THRESHOLD",
26
+ "IDLE_CONSECUTIVE_POLLS",
27
+ "MEM_WARNING_PCT",
28
+ "MEM_CRITICAL_PCT",
29
+ "_format_uptime",
30
+ "_make_bar",
31
+ "_cpu_color",
32
+ "_mem_color",
33
+ ]