eeroctl 1.7.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.
- eeroctl/__init__.py +19 -0
- eeroctl/commands/__init__.py +32 -0
- eeroctl/commands/activity.py +237 -0
- eeroctl/commands/auth.py +471 -0
- eeroctl/commands/completion.py +142 -0
- eeroctl/commands/device.py +492 -0
- eeroctl/commands/eero/__init__.py +12 -0
- eeroctl/commands/eero/base.py +224 -0
- eeroctl/commands/eero/led.py +154 -0
- eeroctl/commands/eero/nightlight.py +235 -0
- eeroctl/commands/eero/updates.py +82 -0
- eeroctl/commands/network/__init__.py +18 -0
- eeroctl/commands/network/advanced.py +191 -0
- eeroctl/commands/network/backup.py +162 -0
- eeroctl/commands/network/base.py +331 -0
- eeroctl/commands/network/dhcp.py +118 -0
- eeroctl/commands/network/dns.py +197 -0
- eeroctl/commands/network/forwards.py +115 -0
- eeroctl/commands/network/guest.py +162 -0
- eeroctl/commands/network/security.py +162 -0
- eeroctl/commands/network/speedtest.py +99 -0
- eeroctl/commands/network/sqm.py +194 -0
- eeroctl/commands/profile.py +671 -0
- eeroctl/commands/troubleshoot.py +317 -0
- eeroctl/context.py +254 -0
- eeroctl/errors.py +156 -0
- eeroctl/exit_codes.py +68 -0
- eeroctl/formatting/__init__.py +90 -0
- eeroctl/formatting/base.py +181 -0
- eeroctl/formatting/device.py +430 -0
- eeroctl/formatting/eero.py +591 -0
- eeroctl/formatting/misc.py +87 -0
- eeroctl/formatting/network.py +659 -0
- eeroctl/formatting/profile.py +443 -0
- eeroctl/main.py +161 -0
- eeroctl/options.py +429 -0
- eeroctl/output.py +739 -0
- eeroctl/safety.py +259 -0
- eeroctl/utils.py +181 -0
- eeroctl-1.7.1.dist-info/METADATA +115 -0
- eeroctl-1.7.1.dist-info/RECORD +45 -0
- eeroctl-1.7.1.dist-info/WHEEL +5 -0
- eeroctl-1.7.1.dist-info/entry_points.txt +3 -0
- eeroctl-1.7.1.dist-info/licenses/LICENSE +21 -0
- eeroctl-1.7.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Troubleshooting commands for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
- eero troubleshoot connectivity: Check connectivity
|
|
5
|
+
- eero troubleshoot ping: Ping a host
|
|
6
|
+
- eero troubleshoot trace: Traceroute to a host
|
|
7
|
+
- eero troubleshoot doctor: Run diagnostic checks
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from eero import EeroClient
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from ..context import ensure_cli_context
|
|
18
|
+
from ..formatting import get_network_status_value
|
|
19
|
+
from ..options import apply_options, network_option, output_option
|
|
20
|
+
from ..utils import with_client
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group(name="troubleshoot")
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def troubleshoot_group(ctx: click.Context) -> None:
|
|
26
|
+
"""Troubleshooting and diagnostics.
|
|
27
|
+
|
|
28
|
+
\b
|
|
29
|
+
Commands:
|
|
30
|
+
connectivity - Check network connectivity
|
|
31
|
+
ping - Ping a target host
|
|
32
|
+
trace - Traceroute to target
|
|
33
|
+
doctor - Run diagnostic checks
|
|
34
|
+
|
|
35
|
+
\b
|
|
36
|
+
Examples:
|
|
37
|
+
eero troubleshoot connectivity
|
|
38
|
+
eero troubleshoot ping --target 8.8.8.8
|
|
39
|
+
eero troubleshoot doctor
|
|
40
|
+
"""
|
|
41
|
+
ensure_cli_context(ctx)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@troubleshoot_group.command(name="connectivity")
|
|
45
|
+
@output_option
|
|
46
|
+
@network_option
|
|
47
|
+
@click.pass_context
|
|
48
|
+
@with_client
|
|
49
|
+
async def troubleshoot_connectivity(
|
|
50
|
+
ctx: click.Context, client: EeroClient, output: Optional[str], network_id: Optional[str]
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Check network connectivity status."""
|
|
53
|
+
cli_ctx = apply_options(ctx, output=output, network_id=network_id)
|
|
54
|
+
console = cli_ctx.console
|
|
55
|
+
|
|
56
|
+
with cli_ctx.status("Checking connectivity..."):
|
|
57
|
+
network = await client.get_network(cli_ctx.network_id)
|
|
58
|
+
diagnostics = await client.get_diagnostics(cli_ctx.network_id)
|
|
59
|
+
|
|
60
|
+
if cli_ctx.is_structured_output():
|
|
61
|
+
data = {
|
|
62
|
+
"network_status": get_network_status_value(network),
|
|
63
|
+
"public_ip": network.public_ip,
|
|
64
|
+
"isp": network.isp_name,
|
|
65
|
+
"diagnostics": diagnostics,
|
|
66
|
+
}
|
|
67
|
+
cli_ctx.render_structured(data, "eero.troubleshoot.connectivity/v1")
|
|
68
|
+
else:
|
|
69
|
+
status = get_network_status_value(network)
|
|
70
|
+
if "online" in status.lower() or "connected" in status.lower():
|
|
71
|
+
status_display = f"[green]{status}[/green]"
|
|
72
|
+
elif "offline" in status.lower():
|
|
73
|
+
status_display = f"[red]{status}[/red]"
|
|
74
|
+
else:
|
|
75
|
+
status_display = f"[yellow]{status}[/yellow]"
|
|
76
|
+
|
|
77
|
+
content = (
|
|
78
|
+
f"[bold]Status:[/bold] {status_display}\n"
|
|
79
|
+
f"[bold]Public IP:[/bold] {network.public_ip or 'N/A'}\n"
|
|
80
|
+
f"[bold]ISP:[/bold] {network.isp_name or 'N/A'}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Add health info if available
|
|
84
|
+
health = getattr(network, "health", {})
|
|
85
|
+
if health:
|
|
86
|
+
internet_status = health.get("internet", {}).get("status", "Unknown")
|
|
87
|
+
content += f"\n[bold]Internet Status:[/bold] {internet_status}"
|
|
88
|
+
|
|
89
|
+
console.print(Panel(content, title="Connectivity Status", border_style="blue"))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@troubleshoot_group.command(name="ping")
|
|
93
|
+
@click.option("--target", "-t", required=True, help="Target host or IP")
|
|
94
|
+
@click.option("--from", "from_eero", help="Eero node to ping from (ID or name)")
|
|
95
|
+
@output_option
|
|
96
|
+
@network_option
|
|
97
|
+
@click.pass_context
|
|
98
|
+
@with_client
|
|
99
|
+
async def troubleshoot_ping(
|
|
100
|
+
ctx: click.Context,
|
|
101
|
+
client: EeroClient,
|
|
102
|
+
target: str,
|
|
103
|
+
from_eero: Optional[str],
|
|
104
|
+
output: Optional[str],
|
|
105
|
+
network_id: Optional[str],
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Ping a target host.
|
|
108
|
+
|
|
109
|
+
Note: This is a placeholder - actual ping functionality
|
|
110
|
+
depends on Eero API support.
|
|
111
|
+
|
|
112
|
+
\b
|
|
113
|
+
Options:
|
|
114
|
+
--target, -t Target host or IP (required)
|
|
115
|
+
--from Eero node to ping from
|
|
116
|
+
"""
|
|
117
|
+
cli_ctx = apply_options(ctx, output=output, network_id=network_id)
|
|
118
|
+
console = cli_ctx.console
|
|
119
|
+
|
|
120
|
+
console.print(
|
|
121
|
+
"[yellow]Note: Direct ping functionality may not be available in Eero API[/yellow]"
|
|
122
|
+
)
|
|
123
|
+
console.print(f"[dim]Target: {target}[/dim]")
|
|
124
|
+
if from_eero:
|
|
125
|
+
console.print(f"[dim]From: {from_eero}[/dim]")
|
|
126
|
+
|
|
127
|
+
with cli_ctx.status("Running diagnostics..."):
|
|
128
|
+
diagnostics = await client.get_diagnostics(cli_ctx.network_id)
|
|
129
|
+
|
|
130
|
+
if cli_ctx.is_structured_output():
|
|
131
|
+
cli_ctx.render_structured(
|
|
132
|
+
{
|
|
133
|
+
"target": target,
|
|
134
|
+
"from_eero": from_eero,
|
|
135
|
+
"note": "Direct ping not available via API",
|
|
136
|
+
"diagnostics": diagnostics,
|
|
137
|
+
},
|
|
138
|
+
"eero.troubleshoot.ping/v1",
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
console.print(
|
|
142
|
+
Panel(
|
|
143
|
+
f"[bold]Target:[/bold] {target}\n"
|
|
144
|
+
f"[bold]From:[/bold] {from_eero or 'gateway'}\n\n"
|
|
145
|
+
"[dim]Ping results would appear here if API supports it.[/dim]\n"
|
|
146
|
+
"[dim]Use `eero troubleshoot doctor` for full diagnostics.[/dim]",
|
|
147
|
+
title="Ping",
|
|
148
|
+
border_style="blue",
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@troubleshoot_group.command(name="trace")
|
|
154
|
+
@click.option("--target", "-t", required=True, help="Target host or IP")
|
|
155
|
+
@click.option("--from", "from_eero", help="Eero node to trace from (ID or name)")
|
|
156
|
+
@output_option
|
|
157
|
+
@network_option
|
|
158
|
+
@click.pass_context
|
|
159
|
+
@with_client
|
|
160
|
+
async def troubleshoot_trace(
|
|
161
|
+
ctx: click.Context,
|
|
162
|
+
client: EeroClient,
|
|
163
|
+
target: str,
|
|
164
|
+
from_eero: Optional[str],
|
|
165
|
+
output: Optional[str],
|
|
166
|
+
network_id: Optional[str],
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Traceroute to a target host.
|
|
169
|
+
|
|
170
|
+
Note: This is a placeholder - actual traceroute functionality
|
|
171
|
+
depends on Eero API support.
|
|
172
|
+
|
|
173
|
+
\b
|
|
174
|
+
Options:
|
|
175
|
+
--target, -t Target host or IP (required)
|
|
176
|
+
--from Eero node to trace from
|
|
177
|
+
"""
|
|
178
|
+
cli_ctx = apply_options(ctx, output=output, network_id=network_id)
|
|
179
|
+
console = cli_ctx.console
|
|
180
|
+
|
|
181
|
+
console.print(
|
|
182
|
+
"[yellow]Note: Direct traceroute functionality may not be available in Eero API[/yellow]"
|
|
183
|
+
)
|
|
184
|
+
console.print(f"[dim]Target: {target}[/dim]")
|
|
185
|
+
|
|
186
|
+
with cli_ctx.status("Running diagnostics..."):
|
|
187
|
+
# diagnostics not currently used but may be needed for full trace support
|
|
188
|
+
_ = await client.get_diagnostics(cli_ctx.network_id)
|
|
189
|
+
routing = await client.get_routing(cli_ctx.network_id)
|
|
190
|
+
|
|
191
|
+
if cli_ctx.is_structured_output():
|
|
192
|
+
cli_ctx.render_structured(
|
|
193
|
+
{
|
|
194
|
+
"target": target,
|
|
195
|
+
"from_eero": from_eero,
|
|
196
|
+
"note": "Direct traceroute not available via API",
|
|
197
|
+
"routing": routing,
|
|
198
|
+
},
|
|
199
|
+
"eero.troubleshoot.trace/v1",
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
console.print(
|
|
203
|
+
Panel(
|
|
204
|
+
f"[bold]Target:[/bold] {target}\n"
|
|
205
|
+
f"[bold]From:[/bold] {from_eero or 'gateway'}\n\n"
|
|
206
|
+
"[dim]Traceroute results would appear here if API supports it.[/dim]\n"
|
|
207
|
+
"[dim]Use `eero network routing` for routing information.[/dim]",
|
|
208
|
+
title="Traceroute",
|
|
209
|
+
border_style="blue",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@troubleshoot_group.command(name="doctor")
|
|
215
|
+
@output_option
|
|
216
|
+
@network_option
|
|
217
|
+
@click.pass_context
|
|
218
|
+
@with_client
|
|
219
|
+
async def troubleshoot_doctor(
|
|
220
|
+
ctx: click.Context, client: EeroClient, output: Optional[str], network_id: Optional[str]
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Run diagnostic checks on the network.
|
|
223
|
+
|
|
224
|
+
Performs a comprehensive health check of your Eero network
|
|
225
|
+
including connectivity, mesh health, and configuration.
|
|
226
|
+
"""
|
|
227
|
+
cli_ctx = apply_options(ctx, output=output, network_id=network_id)
|
|
228
|
+
console = cli_ctx.console
|
|
229
|
+
|
|
230
|
+
checks = []
|
|
231
|
+
|
|
232
|
+
with cli_ctx.status("Running diagnostics..."):
|
|
233
|
+
# Check network status
|
|
234
|
+
try:
|
|
235
|
+
network = await client.get_network(cli_ctx.network_id)
|
|
236
|
+
status = get_network_status_value(network)
|
|
237
|
+
if "online" in status.lower() or "connected" in status.lower():
|
|
238
|
+
checks.append(("Network Status", "pass", status))
|
|
239
|
+
else:
|
|
240
|
+
checks.append(("Network Status", "fail", status))
|
|
241
|
+
except Exception as e:
|
|
242
|
+
checks.append(("Network Status", "fail", str(e)))
|
|
243
|
+
|
|
244
|
+
# Check eeros
|
|
245
|
+
try:
|
|
246
|
+
eeros = await client.get_eeros(cli_ctx.network_id)
|
|
247
|
+
online_count = sum(1 for e in eeros if e.status == "green")
|
|
248
|
+
total_count = len(eeros)
|
|
249
|
+
if online_count == total_count:
|
|
250
|
+
checks.append(("Mesh Nodes", "pass", f"{online_count}/{total_count} online"))
|
|
251
|
+
elif online_count > 0:
|
|
252
|
+
checks.append(("Mesh Nodes", "warn", f"{online_count}/{total_count} online"))
|
|
253
|
+
else:
|
|
254
|
+
checks.append(("Mesh Nodes", "fail", f"0/{total_count} online"))
|
|
255
|
+
except Exception as e:
|
|
256
|
+
checks.append(("Mesh Nodes", "fail", str(e)))
|
|
257
|
+
|
|
258
|
+
# Check devices
|
|
259
|
+
try:
|
|
260
|
+
devices = await client.get_devices(cli_ctx.network_id)
|
|
261
|
+
connected = sum(1 for d in devices if d.connected)
|
|
262
|
+
checks.append(("Connected Devices", "info", f"{connected} devices"))
|
|
263
|
+
except Exception as e:
|
|
264
|
+
checks.append(("Connected Devices", "warn", str(e)))
|
|
265
|
+
|
|
266
|
+
# Check diagnostics
|
|
267
|
+
try:
|
|
268
|
+
_ = await client.get_diagnostics(cli_ctx.network_id)
|
|
269
|
+
checks.append(("Diagnostics API", "pass", "Available"))
|
|
270
|
+
except Exception:
|
|
271
|
+
checks.append(("Diagnostics API", "warn", "Not available"))
|
|
272
|
+
|
|
273
|
+
# Check premium status
|
|
274
|
+
try:
|
|
275
|
+
is_premium = await client.is_premium(cli_ctx.network_id)
|
|
276
|
+
checks.append(("Eero Plus", "info", "Active" if is_premium else "Not active"))
|
|
277
|
+
except Exception:
|
|
278
|
+
checks.append(("Eero Plus", "info", "Unknown"))
|
|
279
|
+
|
|
280
|
+
if cli_ctx.is_structured_output():
|
|
281
|
+
data = {
|
|
282
|
+
"checks": [
|
|
283
|
+
{"name": name, "status": status, "message": msg} for name, status, msg in checks
|
|
284
|
+
],
|
|
285
|
+
"overall": "pass" if all(s != "fail" for _, s, _ in checks) else "fail",
|
|
286
|
+
}
|
|
287
|
+
cli_ctx.render_structured(data, "eero.troubleshoot.doctor/v1")
|
|
288
|
+
else:
|
|
289
|
+
table = Table(title="Network Health Check")
|
|
290
|
+
table.add_column("Check", style="cyan")
|
|
291
|
+
table.add_column("Status", justify="center")
|
|
292
|
+
table.add_column("Details")
|
|
293
|
+
|
|
294
|
+
for name, status, message in checks:
|
|
295
|
+
if status == "pass":
|
|
296
|
+
status_display = "[green]✓ PASS[/green]"
|
|
297
|
+
elif status == "fail":
|
|
298
|
+
status_display = "[red]✗ FAIL[/red]"
|
|
299
|
+
elif status == "warn":
|
|
300
|
+
status_display = "[yellow]⚠ WARN[/yellow]"
|
|
301
|
+
else:
|
|
302
|
+
status_display = "[blue]ℹ INFO[/blue]"
|
|
303
|
+
|
|
304
|
+
table.add_row(name, status_display, message)
|
|
305
|
+
|
|
306
|
+
console.print(table)
|
|
307
|
+
|
|
308
|
+
# Overall status
|
|
309
|
+
has_failures = any(s == "fail" for _, s, _ in checks)
|
|
310
|
+
has_warnings = any(s == "warn" for _, s, _ in checks)
|
|
311
|
+
|
|
312
|
+
if has_failures:
|
|
313
|
+
console.print("\n[bold red]⚠ Issues detected. Review the checks above.[/bold red]")
|
|
314
|
+
elif has_warnings:
|
|
315
|
+
console.print("\n[bold yellow]⚠ Some warnings detected.[/bold yellow]")
|
|
316
|
+
else:
|
|
317
|
+
console.print("\n[bold green]✓ All checks passed![/bold green]")
|
eeroctl/context.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""CLI context management.
|
|
2
|
+
|
|
3
|
+
This module provides a context object that is passed through Click commands
|
|
4
|
+
to share state like the client, output settings, and global flags.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from contextlib import nullcontext
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING, Any, ContextManager, Dict, Optional
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from eero import EeroClient
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from .output import DetailLevel, OutputContext, OutputFormat, OutputManager, OutputRenderer
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class EeroCliContext:
|
|
23
|
+
"""Context object to pass shared resources to Click commands."""
|
|
24
|
+
|
|
25
|
+
# Core components
|
|
26
|
+
client: Optional[EeroClient] = None
|
|
27
|
+
console: Console = field(default_factory=Console)
|
|
28
|
+
err_console: Console = field(default_factory=lambda: Console(stderr=True))
|
|
29
|
+
output_manager: Optional[OutputManager] = None
|
|
30
|
+
|
|
31
|
+
# Network selection
|
|
32
|
+
network_id: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
# Output settings
|
|
35
|
+
output_format: str = "table" # table, list, json
|
|
36
|
+
detail_level: str = "brief" # brief, full
|
|
37
|
+
|
|
38
|
+
# Safety and interaction flags
|
|
39
|
+
non_interactive: bool = False
|
|
40
|
+
force: bool = False
|
|
41
|
+
dry_run: bool = False
|
|
42
|
+
quiet: bool = False
|
|
43
|
+
no_color: bool = False
|
|
44
|
+
|
|
45
|
+
# Debug/logging flags
|
|
46
|
+
debug: bool = False
|
|
47
|
+
verbose: bool = False
|
|
48
|
+
|
|
49
|
+
# Retry/timeout settings
|
|
50
|
+
timeout: Optional[int] = None
|
|
51
|
+
retries: Optional[int] = None
|
|
52
|
+
retry_backoff: Optional[int] = None
|
|
53
|
+
|
|
54
|
+
# Additional storage for subcommand state
|
|
55
|
+
_extra: Dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
# Cached renderer instance
|
|
58
|
+
_renderer: Optional[OutputRenderer] = field(default=None, repr=False)
|
|
59
|
+
|
|
60
|
+
def __post_init__(self):
|
|
61
|
+
"""Initialize derived components."""
|
|
62
|
+
if self.output_manager is None:
|
|
63
|
+
self.output_manager = OutputManager(self.console)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def renderer(self) -> OutputRenderer:
|
|
67
|
+
"""Get the output renderer for this context."""
|
|
68
|
+
if self._renderer is None:
|
|
69
|
+
output_ctx = OutputContext(
|
|
70
|
+
format=(
|
|
71
|
+
OutputFormat(self.output_format)
|
|
72
|
+
if self.output_format in ("table", "list", "json", "yaml", "text")
|
|
73
|
+
else OutputFormat.TABLE
|
|
74
|
+
),
|
|
75
|
+
detail=(
|
|
76
|
+
DetailLevel(self.detail_level)
|
|
77
|
+
if self.detail_level in ("brief", "full")
|
|
78
|
+
else DetailLevel.BRIEF
|
|
79
|
+
),
|
|
80
|
+
quiet=self.quiet,
|
|
81
|
+
no_color=self.no_color,
|
|
82
|
+
network_id=self.network_id,
|
|
83
|
+
)
|
|
84
|
+
self._renderer = OutputRenderer(output_ctx)
|
|
85
|
+
return self._renderer
|
|
86
|
+
|
|
87
|
+
def is_json_output(self) -> bool:
|
|
88
|
+
"""Check if output format is JSON."""
|
|
89
|
+
return self.output_format == "json"
|
|
90
|
+
|
|
91
|
+
def is_yaml_output(self) -> bool:
|
|
92
|
+
"""Check if output format is YAML."""
|
|
93
|
+
return self.output_format == "yaml"
|
|
94
|
+
|
|
95
|
+
def is_text_output(self) -> bool:
|
|
96
|
+
"""Check if output format is plain text."""
|
|
97
|
+
return self.output_format == "text"
|
|
98
|
+
|
|
99
|
+
def is_structured_output(self) -> bool:
|
|
100
|
+
"""Check if output format is a structured format (JSON, YAML, or text)."""
|
|
101
|
+
return self.output_format in ("json", "yaml", "text")
|
|
102
|
+
|
|
103
|
+
def status(self, message: str) -> ContextManager:
|
|
104
|
+
"""Return a status spinner context manager, but only for table output.
|
|
105
|
+
|
|
106
|
+
For parseable outputs (json, yaml, list, text), returns a no-op context
|
|
107
|
+
to avoid polluting the output with status messages.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
message: Status message to display
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Status context manager or nullcontext
|
|
114
|
+
"""
|
|
115
|
+
if self.output_format == "table":
|
|
116
|
+
return self.console.status(message)
|
|
117
|
+
return nullcontext()
|
|
118
|
+
|
|
119
|
+
def render_structured(self, data: Any, schema: str) -> None:
|
|
120
|
+
"""Render data in the current structured format (JSON, YAML, or text).
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
data: Data to render (dict or list)
|
|
124
|
+
schema: Schema identifier for envelope
|
|
125
|
+
"""
|
|
126
|
+
if self.is_json_output():
|
|
127
|
+
self.renderer.render_json(data, schema)
|
|
128
|
+
elif self.is_yaml_output():
|
|
129
|
+
self.renderer.render_yaml(data, schema)
|
|
130
|
+
else:
|
|
131
|
+
self.renderer.render_text(data, schema)
|
|
132
|
+
|
|
133
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
134
|
+
"""Get a value from extra storage."""
|
|
135
|
+
return self._extra.get(key, default)
|
|
136
|
+
|
|
137
|
+
def set(self, key: str, value: Any) -> None:
|
|
138
|
+
"""Set a value in extra storage."""
|
|
139
|
+
self._extra[key] = value
|
|
140
|
+
|
|
141
|
+
def __getitem__(self, key: str) -> Any:
|
|
142
|
+
"""Get a value from extra storage using [] syntax."""
|
|
143
|
+
return self._extra.get(key)
|
|
144
|
+
|
|
145
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
146
|
+
"""Set a value in extra storage using [] syntax."""
|
|
147
|
+
self._extra[key] = value
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ensure_cli_context(ctx: click.Context) -> EeroCliContext:
|
|
151
|
+
"""Ensure the Click context has an EeroCliContext object.
|
|
152
|
+
|
|
153
|
+
If ctx.obj is None or not an EeroCliContext, creates a new one.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
ctx: Click context
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The EeroCliContext object
|
|
160
|
+
"""
|
|
161
|
+
if ctx.obj is None or not isinstance(ctx.obj, EeroCliContext):
|
|
162
|
+
ctx.obj = EeroCliContext()
|
|
163
|
+
return ctx.obj
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_cli_context(ctx: click.Context) -> EeroCliContext:
|
|
167
|
+
"""Get the EeroCliContext from a Click context.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
ctx: Click context
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The EeroCliContext object
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
RuntimeError: If no EeroCliContext is found
|
|
177
|
+
"""
|
|
178
|
+
if ctx.obj is None:
|
|
179
|
+
# Try to find it in parent contexts
|
|
180
|
+
parent = ctx.parent
|
|
181
|
+
while parent is not None:
|
|
182
|
+
if isinstance(parent.obj, EeroCliContext):
|
|
183
|
+
return parent.obj
|
|
184
|
+
parent = parent.parent
|
|
185
|
+
# Create a default one if not found
|
|
186
|
+
ctx.obj = EeroCliContext()
|
|
187
|
+
|
|
188
|
+
if not isinstance(ctx.obj, EeroCliContext):
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
f"Expected EeroCliContext but got {type(ctx.obj).__name__}. "
|
|
191
|
+
"Make sure the CLI is properly initialized."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return ctx.obj
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def create_cli_context(
|
|
198
|
+
debug: bool = False,
|
|
199
|
+
verbose: bool = False,
|
|
200
|
+
output_format: str = "table",
|
|
201
|
+
detail_level: str = "brief",
|
|
202
|
+
network_id: Optional[str] = None,
|
|
203
|
+
non_interactive: bool = False,
|
|
204
|
+
force: bool = False,
|
|
205
|
+
dry_run: bool = False,
|
|
206
|
+
quiet: bool = False,
|
|
207
|
+
no_color: bool = False,
|
|
208
|
+
timeout: Optional[int] = None,
|
|
209
|
+
retries: Optional[int] = None,
|
|
210
|
+
retry_backoff: Optional[int] = None,
|
|
211
|
+
) -> EeroCliContext:
|
|
212
|
+
"""Create a new CLI context with the given settings.
|
|
213
|
+
|
|
214
|
+
Factory function for creating an EeroCliContext with specific options.
|
|
215
|
+
Used primarily by the main CLI entry point.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
debug: Enable debug logging.
|
|
219
|
+
verbose: Enable verbose output.
|
|
220
|
+
output_format: Output format (table, list, json).
|
|
221
|
+
detail_level: Detail level (brief, full).
|
|
222
|
+
network_id: Network ID to use.
|
|
223
|
+
non_interactive: Never prompt for input.
|
|
224
|
+
force: Skip confirmation prompts.
|
|
225
|
+
dry_run: Show what would happen without making changes.
|
|
226
|
+
quiet: Suppress non-essential output.
|
|
227
|
+
no_color: Disable colored output.
|
|
228
|
+
timeout: Request timeout in seconds.
|
|
229
|
+
retries: Number of retries for failed requests.
|
|
230
|
+
retry_backoff: Backoff time between retries in milliseconds.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
A configured EeroCliContext instance.
|
|
234
|
+
"""
|
|
235
|
+
console = Console(force_terminal=not no_color, no_color=no_color, quiet=quiet)
|
|
236
|
+
|
|
237
|
+
return EeroCliContext(
|
|
238
|
+
client=None, # Will be initialized later
|
|
239
|
+
console=console,
|
|
240
|
+
output_manager=OutputManager(console),
|
|
241
|
+
network_id=network_id,
|
|
242
|
+
output_format=output_format,
|
|
243
|
+
detail_level=detail_level,
|
|
244
|
+
non_interactive=non_interactive,
|
|
245
|
+
force=force,
|
|
246
|
+
dry_run=dry_run,
|
|
247
|
+
quiet=quiet,
|
|
248
|
+
no_color=no_color,
|
|
249
|
+
debug=debug,
|
|
250
|
+
verbose=verbose,
|
|
251
|
+
timeout=timeout,
|
|
252
|
+
retries=retries,
|
|
253
|
+
retry_backoff=retry_backoff,
|
|
254
|
+
)
|