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,443 @@
|
|
|
1
|
+
"""Profile formatting utilities for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
This module provides formatting functions for displaying user profile data.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from eero.models.profile import Profile
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from .base import (
|
|
13
|
+
DetailLevel,
|
|
14
|
+
build_panel,
|
|
15
|
+
console,
|
|
16
|
+
field,
|
|
17
|
+
field_bool,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# ==================== Profile Tables ====================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_profiles_table(profiles: List[Profile]) -> Table:
|
|
24
|
+
"""Create a table displaying profiles.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
profiles: List of Profile objects
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Rich Table object
|
|
31
|
+
"""
|
|
32
|
+
table = Table(title="Profiles")
|
|
33
|
+
table.add_column("ID", style="dim")
|
|
34
|
+
table.add_column("Name", style="cyan")
|
|
35
|
+
table.add_column("State", style="green")
|
|
36
|
+
table.add_column("Paused", style="yellow")
|
|
37
|
+
table.add_column("Schedule", style="magenta")
|
|
38
|
+
table.add_column("Content Filter", style="cyan")
|
|
39
|
+
|
|
40
|
+
for profile in profiles:
|
|
41
|
+
paused_style = "red" if profile.paused else "green"
|
|
42
|
+
schedule_status = "Enabled" if profile.schedule_enabled else "Disabled"
|
|
43
|
+
content_filter_enabled = (
|
|
44
|
+
"Enabled"
|
|
45
|
+
if profile.content_filter and any(vars(profile.content_filter).values())
|
|
46
|
+
else "Disabled"
|
|
47
|
+
)
|
|
48
|
+
state_value = profile.state.value if profile.state else "Unknown"
|
|
49
|
+
|
|
50
|
+
table.add_row(
|
|
51
|
+
profile.id or "N/A",
|
|
52
|
+
profile.name,
|
|
53
|
+
state_value,
|
|
54
|
+
f"[{paused_style}]{'Yes' if profile.paused else 'No'}[/{paused_style}]",
|
|
55
|
+
schedule_status,
|
|
56
|
+
content_filter_enabled,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return table
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_profile_devices_table(devices: List[Dict[str, Any]]) -> Table:
|
|
63
|
+
"""Create a table displaying devices in a profile.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
devices: List of device dictionaries from profile
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Rich Table object
|
|
70
|
+
"""
|
|
71
|
+
table = Table(title="Profile Devices")
|
|
72
|
+
table.add_column("ID", style="dim")
|
|
73
|
+
table.add_column("Name", style="cyan")
|
|
74
|
+
table.add_column("Nickname", style="blue")
|
|
75
|
+
table.add_column("IP", style="green")
|
|
76
|
+
table.add_column("MAC", style="yellow")
|
|
77
|
+
table.add_column("Connected", style="magenta")
|
|
78
|
+
table.add_column("Type", style="cyan")
|
|
79
|
+
table.add_column("Manufacturer", style="green")
|
|
80
|
+
table.add_column("Connection Type", style="blue")
|
|
81
|
+
table.add_column("Eero Location", style="yellow")
|
|
82
|
+
|
|
83
|
+
for device in devices:
|
|
84
|
+
device_id = "Unknown"
|
|
85
|
+
if device.get("url"):
|
|
86
|
+
parts = device["url"].split("/")
|
|
87
|
+
if len(parts) >= 2:
|
|
88
|
+
device_id = parts[-1]
|
|
89
|
+
|
|
90
|
+
device_name = (
|
|
91
|
+
device.get("display_name")
|
|
92
|
+
or device.get("hostname")
|
|
93
|
+
or device.get("nickname")
|
|
94
|
+
or "Unknown"
|
|
95
|
+
)
|
|
96
|
+
connected = device.get("connected", False)
|
|
97
|
+
connected_style = "green" if connected else "red"
|
|
98
|
+
source = device.get("source", {})
|
|
99
|
+
eero_location = source.get("location") if source else "Unknown"
|
|
100
|
+
|
|
101
|
+
table.add_row(
|
|
102
|
+
device_id,
|
|
103
|
+
device_name,
|
|
104
|
+
device.get("nickname") or "",
|
|
105
|
+
device.get("ip") or "Unknown",
|
|
106
|
+
device.get("mac") or "Unknown",
|
|
107
|
+
f"[{connected_style}]{'Yes' if connected else 'No'}[/{connected_style}]",
|
|
108
|
+
device.get("device_type") or "Unknown",
|
|
109
|
+
device.get("manufacturer") or "Unknown",
|
|
110
|
+
device.get("connection_type") or "Unknown",
|
|
111
|
+
eero_location,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return table
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ==================== Profile Brief View Panels ====================
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _profile_basic_panel(profile: Profile, extensive: bool = False) -> Panel:
|
|
121
|
+
"""Build the basic profile info panel."""
|
|
122
|
+
paused_style = "red" if profile.paused else "green"
|
|
123
|
+
|
|
124
|
+
lines = [
|
|
125
|
+
field("Name", profile.name),
|
|
126
|
+
field("ID", profile.id),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# State
|
|
130
|
+
state_value = profile.state.value if profile.state else "Unknown"
|
|
131
|
+
state_style = "green" if state_value == "Active" else "yellow"
|
|
132
|
+
lines.append(f"[bold]State:[/bold] [{state_style}]{state_value}[/{state_style}]")
|
|
133
|
+
|
|
134
|
+
# Paused status
|
|
135
|
+
lines.append(
|
|
136
|
+
f"[bold]Paused:[/bold] [{paused_style}]{'Yes' if profile.paused else 'No'}[/{paused_style}]"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Default profile indicator
|
|
140
|
+
is_default = getattr(profile, "default", False)
|
|
141
|
+
if is_default:
|
|
142
|
+
lines.append("[bold]Default:[/bold] [cyan]Yes[/cyan]")
|
|
143
|
+
|
|
144
|
+
if extensive:
|
|
145
|
+
lines.extend(
|
|
146
|
+
[
|
|
147
|
+
field("Devices", profile.device_count),
|
|
148
|
+
field("Connected Devices", profile.connected_device_count),
|
|
149
|
+
field_bool("Premium Enabled", profile.premium_enabled),
|
|
150
|
+
field_bool("Schedule Enabled", profile.schedule_enabled),
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
lines.append(field_bool("Schedule", profile.schedule_enabled))
|
|
155
|
+
|
|
156
|
+
# Content filter summary
|
|
157
|
+
filter_enabled = bool(profile.content_filter and any(vars(profile.content_filter).values()))
|
|
158
|
+
lines.append(field_bool("Content Filter", filter_enabled))
|
|
159
|
+
|
|
160
|
+
return build_panel(lines, f"Profile: {profile.name}", "blue")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _profile_device_summary_panel(profile: Profile) -> Optional[Panel]:
|
|
164
|
+
"""Build the device summary panel for brief view."""
|
|
165
|
+
devices = profile.devices
|
|
166
|
+
if not devices:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
total_devices = len(devices)
|
|
170
|
+
connected_count = sum(1 for d in devices if d.get("connected", False))
|
|
171
|
+
disconnected_count = total_devices - connected_count
|
|
172
|
+
|
|
173
|
+
# Count by device type
|
|
174
|
+
device_types: Dict[str, int] = {}
|
|
175
|
+
for device in devices:
|
|
176
|
+
device_type = device.get("device_type", "unknown")
|
|
177
|
+
device_types[device_type] = device_types.get(device_type, 0) + 1
|
|
178
|
+
|
|
179
|
+
lines = [
|
|
180
|
+
field("Total Devices", total_devices),
|
|
181
|
+
f"[bold]Connected:[/bold] [green]{connected_count}[/green]",
|
|
182
|
+
f"[bold]Disconnected:[/bold] [dim]{disconnected_count}[/dim]",
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
# Add device type breakdown if there are multiple types
|
|
186
|
+
if len(device_types) > 1:
|
|
187
|
+
lines.append("")
|
|
188
|
+
lines.append("[bold]By Type:[/bold]")
|
|
189
|
+
sorted_types = sorted(device_types.items(), key=lambda x: x[1], reverse=True)
|
|
190
|
+
for dtype, count in sorted_types[:5]:
|
|
191
|
+
dtype_display = dtype.replace("_", " ").title()
|
|
192
|
+
lines.append(f" • {dtype_display}: {count}")
|
|
193
|
+
|
|
194
|
+
return build_panel(lines, "Device Summary", "green")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _profile_content_filters_panel(profile: Profile) -> Optional[Panel]:
|
|
198
|
+
"""Build the content filters panel for brief view."""
|
|
199
|
+
# Try unified_content_filters first
|
|
200
|
+
unified_filters = getattr(profile, "unified_content_filters", None)
|
|
201
|
+
if unified_filters:
|
|
202
|
+
dns_policies = (
|
|
203
|
+
unified_filters.get("dns_policies", {})
|
|
204
|
+
if isinstance(unified_filters, dict)
|
|
205
|
+
else getattr(unified_filters, "dns_policies", {})
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if dns_policies:
|
|
209
|
+
lines = []
|
|
210
|
+
filter_map = {
|
|
211
|
+
"block_gaming_content": "Gaming",
|
|
212
|
+
"block_illegal_content": "Illegal Content",
|
|
213
|
+
"block_messaging_content": "Messaging",
|
|
214
|
+
"block_pornographic_content": "Adult Content",
|
|
215
|
+
"block_social_content": "Social Media",
|
|
216
|
+
"block_shopping_content": "Shopping",
|
|
217
|
+
"block_streaming_content": "Streaming",
|
|
218
|
+
"block_violent_content": "Violent Content",
|
|
219
|
+
"safe_search_enabled": "Safe Search",
|
|
220
|
+
"youtube_restricted": "YouTube Restricted",
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
enabled_filters = []
|
|
224
|
+
for key, label in filter_map.items():
|
|
225
|
+
value = (
|
|
226
|
+
dns_policies.get(key, False)
|
|
227
|
+
if isinstance(dns_policies, dict)
|
|
228
|
+
else getattr(dns_policies, key, False)
|
|
229
|
+
)
|
|
230
|
+
if value:
|
|
231
|
+
enabled_filters.append(label)
|
|
232
|
+
|
|
233
|
+
if enabled_filters:
|
|
234
|
+
lines.append("[bold]Blocked Categories:[/bold]")
|
|
235
|
+
for f in enabled_filters:
|
|
236
|
+
lines.append(f" • [red]{f}[/red]")
|
|
237
|
+
return build_panel(lines, "Content Filters", "yellow")
|
|
238
|
+
|
|
239
|
+
# Fallback to content_filter
|
|
240
|
+
if profile.content_filter:
|
|
241
|
+
filter_enabled = any(vars(profile.content_filter).values())
|
|
242
|
+
if filter_enabled:
|
|
243
|
+
lines = ["[bold]Active Filters:[/bold]"]
|
|
244
|
+
for name, value in vars(profile.content_filter).items():
|
|
245
|
+
if value:
|
|
246
|
+
display_name = " ".join(word.capitalize() for word in name.split("_"))
|
|
247
|
+
lines.append(f" • [red]{display_name}[/red]")
|
|
248
|
+
return build_panel(lines, "Content Filters", "yellow")
|
|
249
|
+
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _profile_dns_security_panel(profile: Profile) -> Optional[Panel]:
|
|
254
|
+
"""Build the DNS security panel for brief view."""
|
|
255
|
+
premium_dns = getattr(profile, "premium_dns", None)
|
|
256
|
+
if not premium_dns:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
dns_policies = (
|
|
260
|
+
premium_dns.get("dns_policies", {})
|
|
261
|
+
if isinstance(premium_dns, dict)
|
|
262
|
+
else getattr(premium_dns, "dns_policies", {})
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
ad_block = (
|
|
266
|
+
premium_dns.get("ad_block_settings", {})
|
|
267
|
+
if isinstance(premium_dns, dict)
|
|
268
|
+
else getattr(premium_dns, "ad_block_settings", {})
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
lines = []
|
|
272
|
+
|
|
273
|
+
# DNS policies
|
|
274
|
+
policy_map = {
|
|
275
|
+
"block_pornographic_content": "Block Adult",
|
|
276
|
+
"block_illegal_content": "Block Illegal",
|
|
277
|
+
"block_violent_content": "Block Violent",
|
|
278
|
+
"safe_search_enabled": "Safe Search",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for key, label in policy_map.items():
|
|
282
|
+
value = (
|
|
283
|
+
dns_policies.get(key, False)
|
|
284
|
+
if isinstance(dns_policies, dict)
|
|
285
|
+
else getattr(dns_policies, key, False)
|
|
286
|
+
)
|
|
287
|
+
if value:
|
|
288
|
+
lines.append(f"[bold]{label}:[/bold] [green]Enabled[/green]")
|
|
289
|
+
|
|
290
|
+
# Ad blocking
|
|
291
|
+
ad_block_enabled = (
|
|
292
|
+
ad_block.get("enabled", False)
|
|
293
|
+
if isinstance(ad_block, dict)
|
|
294
|
+
else getattr(ad_block, "enabled", False)
|
|
295
|
+
)
|
|
296
|
+
if ad_block_enabled:
|
|
297
|
+
lines.append("[bold]Ad Block:[/bold] [green]Enabled[/green]")
|
|
298
|
+
|
|
299
|
+
# DNS provider
|
|
300
|
+
dns_provider = (
|
|
301
|
+
premium_dns.get("dns_provider")
|
|
302
|
+
if isinstance(premium_dns, dict)
|
|
303
|
+
else getattr(premium_dns, "dns_provider", None)
|
|
304
|
+
)
|
|
305
|
+
if dns_provider:
|
|
306
|
+
lines.append(field("DNS Provider", dns_provider))
|
|
307
|
+
|
|
308
|
+
return build_panel(lines, "DNS Security", "magenta") if lines else None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _profile_blocked_apps_panel(profile: Profile) -> Optional[Panel]:
|
|
312
|
+
"""Build the blocked applications panel."""
|
|
313
|
+
premium_dns = getattr(profile, "premium_dns", None)
|
|
314
|
+
if not premium_dns:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
blocked_apps = (
|
|
318
|
+
premium_dns.get("blocked_applications", [])
|
|
319
|
+
if isinstance(premium_dns, dict)
|
|
320
|
+
else getattr(premium_dns, "blocked_applications", [])
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if not blocked_apps:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
lines = ["[bold]Blocked Applications:[/bold]"] + [f" • {app}" for app in blocked_apps]
|
|
327
|
+
return build_panel(lines, "Blocked Apps", "red")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _profile_custom_lists_panel(profile: Profile) -> Optional[Panel]:
|
|
331
|
+
"""Build the custom block/allow lists panel for brief view."""
|
|
332
|
+
block_list = profile.custom_block_list or []
|
|
333
|
+
allow_list = profile.custom_allow_list or []
|
|
334
|
+
|
|
335
|
+
if not block_list and not allow_list:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
lines = []
|
|
339
|
+
|
|
340
|
+
if block_list:
|
|
341
|
+
lines.append(f"[bold]Blocked Domains:[/bold] [red]{len(block_list)}[/red]")
|
|
342
|
+
for domain in block_list[:3]:
|
|
343
|
+
lines.append(f" • {domain}")
|
|
344
|
+
if len(block_list) > 3:
|
|
345
|
+
lines.append(f" [dim]... and {len(block_list) - 3} more[/dim]")
|
|
346
|
+
|
|
347
|
+
if allow_list:
|
|
348
|
+
if lines:
|
|
349
|
+
lines.append("")
|
|
350
|
+
lines.append(f"[bold]Allowed Domains:[/bold] [green]{len(allow_list)}[/green]")
|
|
351
|
+
for domain in allow_list[:3]:
|
|
352
|
+
lines.append(f" • {domain}")
|
|
353
|
+
if len(allow_list) > 3:
|
|
354
|
+
lines.append(f" [dim]... and {len(allow_list) - 3} more[/dim]")
|
|
355
|
+
|
|
356
|
+
return build_panel(lines, "Custom Lists", "cyan")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _profile_schedule_panel(profile: Profile) -> Optional[Panel]:
|
|
360
|
+
"""Build the schedule panel."""
|
|
361
|
+
if not profile.schedule_enabled or not profile.schedule_blocks:
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
lines = [
|
|
365
|
+
f"[bold]{', '.join(block.days)}:[/bold] {block.start_time} - {block.end_time}"
|
|
366
|
+
for block in profile.schedule_blocks
|
|
367
|
+
]
|
|
368
|
+
return build_panel(lines, "Schedule", "green")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ==================== Main Profile Details Function ====================
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def print_profile_details(profile: Profile, detail_level: DetailLevel = "brief") -> None:
|
|
375
|
+
"""Print profile information with configurable detail level.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
profile: Profile object
|
|
379
|
+
detail_level: "brief" or "full"
|
|
380
|
+
"""
|
|
381
|
+
extensive = detail_level == "full"
|
|
382
|
+
|
|
383
|
+
# Basic info panel (always shown)
|
|
384
|
+
console.print(_profile_basic_panel(profile, extensive))
|
|
385
|
+
|
|
386
|
+
if not extensive:
|
|
387
|
+
# Brief view panels
|
|
388
|
+
|
|
389
|
+
# Device summary
|
|
390
|
+
summary_panel = _profile_device_summary_panel(profile)
|
|
391
|
+
if summary_panel:
|
|
392
|
+
console.print(summary_panel)
|
|
393
|
+
|
|
394
|
+
# Content filters
|
|
395
|
+
filters_panel = _profile_content_filters_panel(profile)
|
|
396
|
+
if filters_panel:
|
|
397
|
+
console.print(filters_panel)
|
|
398
|
+
|
|
399
|
+
# DNS security
|
|
400
|
+
dns_panel = _profile_dns_security_panel(profile)
|
|
401
|
+
if dns_panel:
|
|
402
|
+
console.print(dns_panel)
|
|
403
|
+
|
|
404
|
+
# Blocked apps
|
|
405
|
+
apps_panel = _profile_blocked_apps_panel(profile)
|
|
406
|
+
if apps_panel:
|
|
407
|
+
console.print(apps_panel)
|
|
408
|
+
|
|
409
|
+
# Custom lists
|
|
410
|
+
lists_panel = _profile_custom_lists_panel(profile)
|
|
411
|
+
if lists_panel:
|
|
412
|
+
console.print(lists_panel)
|
|
413
|
+
|
|
414
|
+
# Devices table
|
|
415
|
+
if profile.devices:
|
|
416
|
+
console.print(create_profile_devices_table(profile.devices))
|
|
417
|
+
else:
|
|
418
|
+
console.print("[bold yellow]No devices in this profile[/bold yellow]")
|
|
419
|
+
|
|
420
|
+
else:
|
|
421
|
+
# Extensive view panels
|
|
422
|
+
|
|
423
|
+
# Schedule
|
|
424
|
+
schedule_panel = _profile_schedule_panel(profile)
|
|
425
|
+
if schedule_panel:
|
|
426
|
+
console.print(schedule_panel)
|
|
427
|
+
|
|
428
|
+
# Content filter details
|
|
429
|
+
if profile.content_filter:
|
|
430
|
+
filter_enabled = any(vars(profile.content_filter).values())
|
|
431
|
+
if filter_enabled:
|
|
432
|
+
filter_settings = []
|
|
433
|
+
for name, value in vars(profile.content_filter).items():
|
|
434
|
+
if value:
|
|
435
|
+
display_name = " ".join(word.capitalize() for word in name.split("_"))
|
|
436
|
+
filter_settings.append(f"[bold]{display_name}:[/bold] Enabled")
|
|
437
|
+
console.print(build_panel(filter_settings, "Content Filtering", "yellow"))
|
|
438
|
+
|
|
439
|
+
# Block/Allow lists (full)
|
|
440
|
+
if profile.custom_block_list:
|
|
441
|
+
console.print(build_panel(profile.custom_block_list, "Blocked Domains", "red"))
|
|
442
|
+
if profile.custom_allow_list:
|
|
443
|
+
console.print(build_panel(profile.custom_allow_list, "Allowed Domains", "green"))
|
eeroctl/main.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""CLI entry point with noun-first command structure.
|
|
2
|
+
|
|
3
|
+
This module provides the main CLI entry point with global flags
|
|
4
|
+
and registers command groups from the commands/ module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import eero
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from .commands import (
|
|
17
|
+
activity_group,
|
|
18
|
+
auth_group,
|
|
19
|
+
completion_group,
|
|
20
|
+
device_group,
|
|
21
|
+
eero_group,
|
|
22
|
+
network_group,
|
|
23
|
+
profile_group,
|
|
24
|
+
troubleshoot_group,
|
|
25
|
+
)
|
|
26
|
+
from .context import create_cli_context
|
|
27
|
+
from .utils import get_preferred_network
|
|
28
|
+
|
|
29
|
+
_LOGGER = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ==================== Version Info ====================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_version_info() -> str:
|
|
36
|
+
"""Build version string with Python and eero-api versions."""
|
|
37
|
+
eeroctl_version = version("eeroctl")
|
|
38
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
39
|
+
return f"%(prog)s {eeroctl_version}\nPython {python_version}\neero-api {eero.__version__}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ==================== Main CLI Group ====================
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.group(invoke_without_command=True)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--debug",
|
|
48
|
+
is_flag=True,
|
|
49
|
+
help="Enable debug logging.",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--quiet",
|
|
53
|
+
"-q",
|
|
54
|
+
is_flag=True,
|
|
55
|
+
help="Suppress non-essential output.",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--no-color",
|
|
59
|
+
is_flag=True,
|
|
60
|
+
help="Disable colored output.",
|
|
61
|
+
)
|
|
62
|
+
@click.option(
|
|
63
|
+
"--output",
|
|
64
|
+
"-o",
|
|
65
|
+
type=click.Choice(["table", "list", "json", "yaml", "text"]),
|
|
66
|
+
default="table",
|
|
67
|
+
help="Output format (default: table).",
|
|
68
|
+
)
|
|
69
|
+
@click.option(
|
|
70
|
+
"--network-id",
|
|
71
|
+
"-n",
|
|
72
|
+
help="Network ID to operate on.",
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--non-interactive",
|
|
76
|
+
is_flag=True,
|
|
77
|
+
help="Never prompt for input; fail if confirmation required.",
|
|
78
|
+
)
|
|
79
|
+
@click.option(
|
|
80
|
+
"--force",
|
|
81
|
+
"-y",
|
|
82
|
+
"--yes",
|
|
83
|
+
is_flag=True,
|
|
84
|
+
help="Skip confirmation prompts for disruptive actions.",
|
|
85
|
+
)
|
|
86
|
+
@click.version_option(message=_get_version_info())
|
|
87
|
+
@click.pass_context
|
|
88
|
+
def cli(
|
|
89
|
+
ctx: click.Context,
|
|
90
|
+
debug: bool,
|
|
91
|
+
quiet: bool,
|
|
92
|
+
no_color: bool,
|
|
93
|
+
output: str,
|
|
94
|
+
network_id: Optional[str],
|
|
95
|
+
non_interactive: bool,
|
|
96
|
+
force: bool,
|
|
97
|
+
):
|
|
98
|
+
"""Eero network management CLI.
|
|
99
|
+
|
|
100
|
+
Manage your Eero mesh Wi-Fi network from the command line.
|
|
101
|
+
Use --help with any command for more information.
|
|
102
|
+
"""
|
|
103
|
+
# Setup logging
|
|
104
|
+
if debug:
|
|
105
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
106
|
+
else:
|
|
107
|
+
logging.basicConfig(level=logging.WARNING)
|
|
108
|
+
|
|
109
|
+
# Create console
|
|
110
|
+
console = Console(force_terminal=not no_color, no_color=no_color, quiet=quiet)
|
|
111
|
+
|
|
112
|
+
# Create context
|
|
113
|
+
cli_ctx = create_cli_context(
|
|
114
|
+
debug=debug,
|
|
115
|
+
quiet=quiet,
|
|
116
|
+
no_color=no_color,
|
|
117
|
+
output_format=output,
|
|
118
|
+
non_interactive=non_interactive,
|
|
119
|
+
force=force,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Override console with the configured one
|
|
123
|
+
cli_ctx.console = console
|
|
124
|
+
|
|
125
|
+
# Load preferred network if not specified
|
|
126
|
+
if network_id:
|
|
127
|
+
cli_ctx.network_id = network_id
|
|
128
|
+
else:
|
|
129
|
+
preferred_network = get_preferred_network()
|
|
130
|
+
if preferred_network:
|
|
131
|
+
cli_ctx.network_id = preferred_network
|
|
132
|
+
|
|
133
|
+
ctx.obj = cli_ctx
|
|
134
|
+
|
|
135
|
+
# Show help if no command specified
|
|
136
|
+
if ctx.invoked_subcommand is None:
|
|
137
|
+
click.echo(ctx.get_help())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ==================== Register Command Groups ====================
|
|
141
|
+
|
|
142
|
+
cli.add_command(auth_group, name="auth")
|
|
143
|
+
cli.add_command(network_group, name="network")
|
|
144
|
+
cli.add_command(eero_group, name="eero")
|
|
145
|
+
cli.add_command(device_group, name="device")
|
|
146
|
+
cli.add_command(profile_group, name="profile")
|
|
147
|
+
cli.add_command(activity_group, name="activity")
|
|
148
|
+
cli.add_command(troubleshoot_group, name="troubleshoot")
|
|
149
|
+
cli.add_command(completion_group, name="completion")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ==================== Entry Point ====================
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main():
|
|
156
|
+
"""Main entry point for the CLI."""
|
|
157
|
+
cli()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|