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
eeroctl/safety.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Safety middleware for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
Handles confirmation prompts for destructive/disruptive operations.
|
|
4
|
+
Provides consistent safety behavior across all mutating commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Callable, Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.prompt import Confirm, Prompt
|
|
15
|
+
|
|
16
|
+
from .exit_codes import ExitCode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OperationRisk(str, Enum):
|
|
20
|
+
"""Risk level of an operation."""
|
|
21
|
+
|
|
22
|
+
LOW = "low"
|
|
23
|
+
"""Low risk - no confirmation needed."""
|
|
24
|
+
|
|
25
|
+
MEDIUM = "medium"
|
|
26
|
+
"""Medium risk - simple Y/N confirmation."""
|
|
27
|
+
|
|
28
|
+
HIGH = "high"
|
|
29
|
+
"""High risk - requires typed confirmation phrase."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SafetyContext:
|
|
34
|
+
"""Context for safety checks."""
|
|
35
|
+
|
|
36
|
+
force: bool = False
|
|
37
|
+
"""Whether --force was specified."""
|
|
38
|
+
|
|
39
|
+
non_interactive: bool = False
|
|
40
|
+
"""Whether --non-interactive was specified."""
|
|
41
|
+
|
|
42
|
+
dry_run: bool = False
|
|
43
|
+
"""Whether --dry-run was specified."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SafetyError(Exception):
|
|
47
|
+
"""Raised when a safety check fails."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str, exit_code: int = ExitCode.SAFETY_RAIL):
|
|
50
|
+
self.message = message
|
|
51
|
+
self.exit_code = exit_code
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def require_confirmation(
|
|
56
|
+
action: str,
|
|
57
|
+
target: str,
|
|
58
|
+
risk: OperationRisk = OperationRisk.MEDIUM,
|
|
59
|
+
confirmation_phrase: Optional[str] = None,
|
|
60
|
+
ctx: Optional[SafetyContext] = None,
|
|
61
|
+
console: Optional[Console] = None,
|
|
62
|
+
) -> bool:
|
|
63
|
+
"""Check if operation should proceed based on safety settings.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
action: Description of the action (e.g., "reboot")
|
|
67
|
+
target: Target of the action (e.g., "Living Room eero")
|
|
68
|
+
risk: Risk level of the operation
|
|
69
|
+
confirmation_phrase: Required phrase for HIGH risk operations
|
|
70
|
+
ctx: Safety context with force/non_interactive flags
|
|
71
|
+
console: Rich console for prompts
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if operation should proceed
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
SafetyError: If safety check fails and operation should not proceed
|
|
78
|
+
"""
|
|
79
|
+
if ctx is None:
|
|
80
|
+
ctx = SafetyContext()
|
|
81
|
+
|
|
82
|
+
if console is None:
|
|
83
|
+
console = Console(stderr=True)
|
|
84
|
+
|
|
85
|
+
# Dry run always prints and returns False
|
|
86
|
+
if ctx.dry_run:
|
|
87
|
+
console.print(f"[yellow]DRY RUN:[/yellow] Would {action} {target}")
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# Force bypasses all confirmations
|
|
91
|
+
if ctx.force:
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
# Low risk operations proceed without confirmation
|
|
95
|
+
if risk == OperationRisk.LOW:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
# Non-interactive mode without force fails
|
|
99
|
+
if ctx.non_interactive:
|
|
100
|
+
raise SafetyError(
|
|
101
|
+
f"Operation '{action}' on '{target}' requires confirmation. "
|
|
102
|
+
"Use --force to proceed in non-interactive mode.",
|
|
103
|
+
exit_code=ExitCode.SAFETY_RAIL,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Interactive confirmation
|
|
107
|
+
if risk == OperationRisk.HIGH:
|
|
108
|
+
# High risk requires typed phrase
|
|
109
|
+
if confirmation_phrase is None:
|
|
110
|
+
confirmation_phrase = action.upper().replace(" ", "")
|
|
111
|
+
|
|
112
|
+
console.print(
|
|
113
|
+
f"\n[bold yellow]⚠ Warning:[/bold yellow] You are about to {action} {target}."
|
|
114
|
+
)
|
|
115
|
+
console.print(
|
|
116
|
+
"[dim]This is a high-impact operation that may cause service disruption.[/dim]"
|
|
117
|
+
)
|
|
118
|
+
console.print(f"\nTo confirm, type [bold]{confirmation_phrase}[/bold] and press Enter:")
|
|
119
|
+
|
|
120
|
+
user_input = Prompt.ask("Confirmation", console=console)
|
|
121
|
+
|
|
122
|
+
if user_input != confirmation_phrase:
|
|
123
|
+
raise SafetyError(
|
|
124
|
+
f"Confirmation phrase mismatch. Expected '{confirmation_phrase}'.",
|
|
125
|
+
exit_code=ExitCode.SAFETY_RAIL,
|
|
126
|
+
)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
else:
|
|
130
|
+
# Medium risk - simple Y/N
|
|
131
|
+
console.print(f"\n[bold]Proceed with {action} on {target}?[/bold]")
|
|
132
|
+
confirmed = Confirm.ask("Continue", default=False, console=console)
|
|
133
|
+
|
|
134
|
+
if not confirmed:
|
|
135
|
+
raise SafetyError(
|
|
136
|
+
"Operation cancelled by user.",
|
|
137
|
+
exit_code=ExitCode.SAFETY_RAIL,
|
|
138
|
+
)
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def confirm_or_fail(
|
|
143
|
+
action: str,
|
|
144
|
+
target: str,
|
|
145
|
+
risk: OperationRisk = OperationRisk.MEDIUM,
|
|
146
|
+
confirmation_phrase: Optional[str] = None,
|
|
147
|
+
force: bool = False,
|
|
148
|
+
non_interactive: bool = False,
|
|
149
|
+
dry_run: bool = False,
|
|
150
|
+
console: Optional[Console] = None,
|
|
151
|
+
) -> bool:
|
|
152
|
+
"""Convenience function combining context creation and confirmation.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
action: Description of the action
|
|
156
|
+
target: Target of the action
|
|
157
|
+
risk: Risk level
|
|
158
|
+
confirmation_phrase: Required phrase for HIGH risk
|
|
159
|
+
force: --force flag value
|
|
160
|
+
non_interactive: --non-interactive flag value
|
|
161
|
+
dry_run: --dry-run flag value
|
|
162
|
+
console: Rich console for prompts
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
True if operation should proceed
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
SafetyError: If safety check fails
|
|
169
|
+
"""
|
|
170
|
+
ctx = SafetyContext(force=force, non_interactive=non_interactive, dry_run=dry_run)
|
|
171
|
+
return require_confirmation(
|
|
172
|
+
action=action,
|
|
173
|
+
target=target,
|
|
174
|
+
risk=risk,
|
|
175
|
+
confirmation_phrase=confirmation_phrase,
|
|
176
|
+
ctx=ctx,
|
|
177
|
+
console=console,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Decorator for commands that require confirmation
|
|
182
|
+
def requires_confirmation(
|
|
183
|
+
action: str,
|
|
184
|
+
target_param: str = "target",
|
|
185
|
+
risk: OperationRisk = OperationRisk.MEDIUM,
|
|
186
|
+
confirmation_phrase: Optional[str] = None,
|
|
187
|
+
):
|
|
188
|
+
"""Decorator to add confirmation requirement to a command.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
action: Description of the action
|
|
192
|
+
target_param: Name of the parameter containing the target
|
|
193
|
+
risk: Risk level of the operation
|
|
194
|
+
confirmation_phrase: Required phrase for HIGH risk
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
@requires_confirmation("reboot", target_param="eero_id", risk=OperationRisk.MEDIUM)
|
|
198
|
+
def reboot(ctx, eero_id: str, force: bool, non_interactive: bool, dry_run: bool):
|
|
199
|
+
...
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def decorator(func: Callable) -> Callable:
|
|
203
|
+
def wrapper(*args, **kwargs):
|
|
204
|
+
# Extract safety-related kwargs
|
|
205
|
+
force = kwargs.get("force", False)
|
|
206
|
+
non_interactive = kwargs.get("non_interactive", False)
|
|
207
|
+
dry_run = kwargs.get("dry_run", False)
|
|
208
|
+
target = kwargs.get(target_param, "unknown")
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
if confirm_or_fail(
|
|
212
|
+
action=action,
|
|
213
|
+
target=str(target),
|
|
214
|
+
risk=risk,
|
|
215
|
+
confirmation_phrase=confirmation_phrase,
|
|
216
|
+
force=force,
|
|
217
|
+
non_interactive=non_interactive,
|
|
218
|
+
dry_run=dry_run,
|
|
219
|
+
):
|
|
220
|
+
return func(*args, **kwargs)
|
|
221
|
+
except SafetyError as e:
|
|
222
|
+
click.echo(f"Error: {e.message}", err=True)
|
|
223
|
+
sys.exit(e.exit_code)
|
|
224
|
+
|
|
225
|
+
# Preserve function metadata
|
|
226
|
+
wrapper.__name__ = func.__name__
|
|
227
|
+
wrapper.__doc__ = func.__doc__
|
|
228
|
+
return wrapper
|
|
229
|
+
|
|
230
|
+
return decorator
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# List of operations and their risk levels for reference
|
|
234
|
+
OPERATION_RISKS = {
|
|
235
|
+
# HIGH risk - requires typed confirmation
|
|
236
|
+
"reboot_network": OperationRisk.HIGH,
|
|
237
|
+
"change_wifi_password": OperationRisk.HIGH,
|
|
238
|
+
"change_wifi_ssid": OperationRisk.HIGH,
|
|
239
|
+
"factory_reset": OperationRisk.HIGH,
|
|
240
|
+
# MEDIUM risk - Y/N confirmation
|
|
241
|
+
"reboot_eero": OperationRisk.MEDIUM,
|
|
242
|
+
"guest_enable": OperationRisk.MEDIUM,
|
|
243
|
+
"guest_disable": OperationRisk.MEDIUM,
|
|
244
|
+
"dns_change": OperationRisk.MEDIUM,
|
|
245
|
+
"security_change": OperationRisk.MEDIUM,
|
|
246
|
+
"sqm_change": OperationRisk.MEDIUM,
|
|
247
|
+
"block_device": OperationRisk.MEDIUM,
|
|
248
|
+
"unblock_device": OperationRisk.MEDIUM,
|
|
249
|
+
"pause_profile": OperationRisk.MEDIUM,
|
|
250
|
+
"unpause_profile": OperationRisk.MEDIUM,
|
|
251
|
+
"schedule_change": OperationRisk.MEDIUM,
|
|
252
|
+
"delete_forward": OperationRisk.MEDIUM,
|
|
253
|
+
"delete_reservation": OperationRisk.MEDIUM,
|
|
254
|
+
"export_support_bundle": OperationRisk.MEDIUM,
|
|
255
|
+
# LOW risk - no confirmation
|
|
256
|
+
"rename_device": OperationRisk.LOW,
|
|
257
|
+
"rename_network": OperationRisk.LOW, # Actually shown as MEDIUM in spec
|
|
258
|
+
"view_any": OperationRisk.LOW,
|
|
259
|
+
}
|
eeroctl/utils.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Utility functions for the Eero CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Awaitable, Callable, Optional, TypeVar
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from eero import EeroClient
|
|
13
|
+
from eero.exceptions import EeroAuthenticationException
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
# Create console for rich output
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def with_client(func: Callable[..., Awaitable[T]]) -> Callable[..., T]:
|
|
23
|
+
"""Decorator that provides an EeroClient to async Click commands.
|
|
24
|
+
|
|
25
|
+
This decorator eliminates the repetitive boilerplate pattern of:
|
|
26
|
+
async def run_cmd():
|
|
27
|
+
async def inner(client):
|
|
28
|
+
...
|
|
29
|
+
await run_with_client(inner)
|
|
30
|
+
asyncio.run(run_cmd())
|
|
31
|
+
|
|
32
|
+
Instead, you can write:
|
|
33
|
+
@command.command()
|
|
34
|
+
@click.pass_context
|
|
35
|
+
@with_client
|
|
36
|
+
async def my_command(ctx, client, ...):
|
|
37
|
+
# Just do the work
|
|
38
|
+
|
|
39
|
+
The decorator:
|
|
40
|
+
- Creates an EeroClient context
|
|
41
|
+
- Handles authentication errors
|
|
42
|
+
- Runs the async function synchronously via asyncio.run()
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
func: Async function that receives (ctx, client, *args, **kwargs)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Synchronous wrapper function
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@functools.wraps(func)
|
|
52
|
+
def wrapper(*args, **kwargs):
|
|
53
|
+
async def run():
|
|
54
|
+
cookie_file = get_cookie_file()
|
|
55
|
+
try:
|
|
56
|
+
async with EeroClient(cookie_file=str(cookie_file)) as client:
|
|
57
|
+
return await func(*args, client=client, **kwargs)
|
|
58
|
+
except EeroAuthenticationException:
|
|
59
|
+
console.print("[bold red]Not authenticated[/bold red]")
|
|
60
|
+
console.print("Please login first: [bold]eero auth login[/bold]")
|
|
61
|
+
sys.exit(3) # ExitCode.AUTH_REQUIRED
|
|
62
|
+
|
|
63
|
+
return asyncio.run(run())
|
|
64
|
+
|
|
65
|
+
return wrapper
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def output_option(func):
|
|
69
|
+
"""Decorator to add --output option to commands."""
|
|
70
|
+
return click.option(
|
|
71
|
+
"--output",
|
|
72
|
+
type=click.Choice(["brief", "extensive", "json"]),
|
|
73
|
+
default="brief",
|
|
74
|
+
help="Output format (brief, extensive, or json)",
|
|
75
|
+
)(func)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_config_dir() -> Path:
|
|
79
|
+
"""Get the configuration directory.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Path to the configuration directory
|
|
83
|
+
"""
|
|
84
|
+
if os.name == "nt": # Windows
|
|
85
|
+
config_dir = Path(os.environ["APPDATA"]) / "eero-api"
|
|
86
|
+
else:
|
|
87
|
+
config_dir = Path.home() / ".config" / "eero-api"
|
|
88
|
+
|
|
89
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
return config_dir
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_cookie_file() -> Path:
|
|
94
|
+
"""Get the cookie file path.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Path to the cookie file
|
|
98
|
+
"""
|
|
99
|
+
return get_config_dir() / "cookies.json"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_config_file() -> Path:
|
|
103
|
+
"""Get the config file path.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Path to the config file
|
|
107
|
+
"""
|
|
108
|
+
return get_config_dir() / "config.json"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def set_preferred_network(network_id: str) -> None:
|
|
112
|
+
"""Set the preferred network ID in the configuration.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
network_id: The network ID to set as preferred
|
|
116
|
+
"""
|
|
117
|
+
config_file = get_config_file()
|
|
118
|
+
config = {}
|
|
119
|
+
|
|
120
|
+
if config_file.exists():
|
|
121
|
+
try:
|
|
122
|
+
with open(config_file, "r") as f:
|
|
123
|
+
config = json.load(f)
|
|
124
|
+
except (json.JSONDecodeError, IOError):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
config["preferred_network_id"] = network_id
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
with open(config_file, "w") as f:
|
|
131
|
+
json.dump(config, f, indent=2)
|
|
132
|
+
except IOError as e:
|
|
133
|
+
console.print(f"[bold red]Error saving config: {e}[/bold red]")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_preferred_network() -> Optional[str]:
|
|
137
|
+
"""Get the preferred network ID from the configuration.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The preferred network ID or None if not set
|
|
141
|
+
"""
|
|
142
|
+
config_file = get_config_file()
|
|
143
|
+
|
|
144
|
+
if not config_file.exists():
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
with open(config_file, "r") as f:
|
|
149
|
+
config = json.load(f)
|
|
150
|
+
return config.get("preferred_network_id")
|
|
151
|
+
except (json.JSONDecodeError, IOError):
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def run_with_client(func):
|
|
156
|
+
"""Run a function with an EeroClient instance.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
func: Async function that takes an EeroClient as argument
|
|
160
|
+
"""
|
|
161
|
+
cookie_file = get_cookie_file()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
async with EeroClient(cookie_file=str(cookie_file)) as client:
|
|
165
|
+
await func(client)
|
|
166
|
+
except EeroAuthenticationException:
|
|
167
|
+
console.print("[bold red]Not authenticated[/bold red]")
|
|
168
|
+
console.print("Please login first: [bold]eero auth login[/bold]")
|
|
169
|
+
raise SystemExit(1)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def confirm_action(message: str) -> bool:
|
|
173
|
+
"""Ask user to confirm an action.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
message: The message to display
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
True if user confirms, False otherwise
|
|
180
|
+
"""
|
|
181
|
+
return click.confirm(message)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eeroctl
|
|
3
|
+
Version: 1.7.1
|
|
4
|
+
Summary: Command-line interface for Eero network management
|
|
5
|
+
Author: Eero CLI Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/fulviofreitas/eeroctl
|
|
8
|
+
Project-URL: Issues, https://github.com/fulviofreitas/eeroctl/issues
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Home Automation
|
|
18
|
+
Classifier: Topic :: System :: Networking
|
|
19
|
+
Classifier: Environment :: Console
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: eero-api==1.4.3
|
|
25
|
+
Requires-Dist: rich>=13.0.0
|
|
26
|
+
Requires-Dist: click>=8.0.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# 🌐 eeroctl
|
|
39
|
+
|
|
40
|
+
[](https://github.com/fulviofreitas/eeroctl/actions/workflows/ci.yml)
|
|
41
|
+
[](https://pypi.org/project/eeroctl/)
|
|
42
|
+
[](https://github.com/fulviofreitas/homebrew-eeroctl)
|
|
43
|
+
|
|
44
|
+
> Manage your Eero mesh Wi-Fi from the terminal ✨
|
|
45
|
+
|
|
46
|
+
## ⚡ Features
|
|
47
|
+
|
|
48
|
+
- 🧭 **Intuitive commands** — noun-first structure (`eero network list`)
|
|
49
|
+
- 📊 **Multiple formats** — table, JSON, YAML, text
|
|
50
|
+
- 🛡️ **Safety rails** — confirmation for destructive actions
|
|
51
|
+
- 🔧 **Script-friendly** — non-interactive mode + machine-readable output
|
|
52
|
+
- 🐚 **Shell completion** — bash, zsh, fish
|
|
53
|
+
|
|
54
|
+
## 📦 Install
|
|
55
|
+
|
|
56
|
+
### Homebrew
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
brew install fulviofreitas/eeroctl/eeroctl
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### PyPI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install eeroctl
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary>From source</summary>
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/fulviofreitas/eeroctl.git
|
|
73
|
+
cd eeroctl
|
|
74
|
+
uv sync && source .venv/bin/activate
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or with pip:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
81
|
+
pip install -e .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
</details>
|
|
85
|
+
|
|
86
|
+
## 🚀 Quick Start
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
eero auth login # Authenticate
|
|
90
|
+
eero network list # List networks
|
|
91
|
+
eero device list # Connected devices
|
|
92
|
+
eero eero list # Mesh nodes
|
|
93
|
+
eero troubleshoot speedtest --force
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> **Tip:** Both `eero` and `eeroctl` commands are available and work identically.
|
|
97
|
+
|
|
98
|
+
## 📖 Documentation
|
|
99
|
+
|
|
100
|
+
Full documentation lives in the **[Wiki](https://github.com/fulviofreitas/eeroctl/wiki)**:
|
|
101
|
+
|
|
102
|
+
| 📚 Guide | Description |
|
|
103
|
+
|----------|-------------|
|
|
104
|
+
| [CLI Reference](https://github.com/fulviofreitas/eeroctl/wiki/CLI-Reference) | Commands, flags & exit codes |
|
|
105
|
+
| [Usage Examples](https://github.com/fulviofreitas/eeroctl/wiki/Usage-Examples) | Practical examples |
|
|
106
|
+
| [Configuration](https://github.com/fulviofreitas/eeroctl/wiki/Configuration) | Auth storage & env vars |
|
|
107
|
+
| [Troubleshooting](https://github.com/fulviofreitas/eeroctl/wiki/Troubleshooting) | Common issues |
|
|
108
|
+
|
|
109
|
+
## 🔗 Dependencies
|
|
110
|
+
|
|
111
|
+
Built on [eero-api](https://github.com/fulviofreitas/eero-api) for API communication.
|
|
112
|
+
|
|
113
|
+
## 📄 License
|
|
114
|
+
|
|
115
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
eeroctl/__init__.py,sha256=Bck2y5Vmenb-009bMukNb_kT7efmk8c6j0adGiZQRNg,436
|
|
2
|
+
eeroctl/context.py,sha256=fIsjyXbRwe6C-9grZG_lnd5fcj-bqyLb4BhOq6BN5Sk,8077
|
|
3
|
+
eeroctl/errors.py,sha256=UbE4jkMIu3oTtn7-4r6wMdzFI_YaBmboXzXSNE3Yaro,5238
|
|
4
|
+
eeroctl/exit_codes.py,sha256=JK-8OA6jZDWk1cPDVQ94Bz80P5MRnkczFwoLOSailmQ,2008
|
|
5
|
+
eeroctl/main.py,sha256=ER1Ji_wWvfLJV6erduF7sG7pm5NqFuRD7YbwxVSIX3M,3894
|
|
6
|
+
eeroctl/options.py,sha256=YbUeo3OwkCJw2A-HRpGKdpQooP6DsyaI0_rfB-I2sXw,13029
|
|
7
|
+
eeroctl/output.py,sha256=07cxnI_eks3QXDB4Cy5RwiKwrpcunCN8Ibu2cvLqI00,25620
|
|
8
|
+
eeroctl/safety.py,sha256=Qkct4MxE_gUekd36ibqPGvJiyflwH-vl28wsfgj-48g,8107
|
|
9
|
+
eeroctl/utils.py,sha256=n6boUe0VQ1C0z-mLaa1ZRXDvGkKL9wehMOsw1KyO12A,4747
|
|
10
|
+
eeroctl/commands/__init__.py,sha256=yTmjcnPJpGmczatEJrhQvfworH0OBpH2fmg1HVAcYfQ,893
|
|
11
|
+
eeroctl/commands/activity.py,sha256=GvrP2DQBOrJLE2DJP1FOseQJVDwTXyk-G5Nm80LjKzU,7926
|
|
12
|
+
eeroctl/commands/auth.py,sha256=r9hgixgIeyw0464f5WybvFX6zgPbHJHAoD5nAHsEUBc,18139
|
|
13
|
+
eeroctl/commands/completion.py,sha256=EjpmmN7QY8z1eitoc212mw8jEIA3or7ZlWkbHjqEYiI,3522
|
|
14
|
+
eeroctl/commands/device.py,sha256=0uAfU7mV15yfnMOd4emohx1P4VP7T_hxlve772yoqFQ,16760
|
|
15
|
+
eeroctl/commands/profile.py,sha256=mUOvEwT02TgL9RLM4-fRtY3cSZpk1F6mQARzqWl4LRE,23180
|
|
16
|
+
eeroctl/commands/troubleshoot.py,sha256=OZFMt_ZQrgtwT5YuYRht-EWV1ROEMtiE889hw-SAXWY,11043
|
|
17
|
+
eeroctl/commands/eero/__init__.py,sha256=l7K7-Bvw_-GMAepCohmjpy038GC451yM-rphjXbgEyQ,354
|
|
18
|
+
eeroctl/commands/eero/base.py,sha256=xT6Jt8zpRNhI4_Bf7AyKgg0a23EGbOA0Zr2Ktfp4miQ,7883
|
|
19
|
+
eeroctl/commands/eero/led.py,sha256=DnkGp6eXMUm8nKMBk5RgtjjdYXtH-uh2di4lN2HNbPI,5209
|
|
20
|
+
eeroctl/commands/eero/nightlight.py,sha256=BUa1jwQERri4m4lqVtDMHKInI6Rv49yWk7Ka1QMyuOo,9095
|
|
21
|
+
eeroctl/commands/eero/updates.py,sha256=9NWb5jYfmA8NTRjvTtdURb-TLucMAlsp9CxHja6nkwk,2460
|
|
22
|
+
eeroctl/commands/network/__init__.py,sha256=0mlN6w5Hl5npgZWbBPCQLwUU6sQNVoMGHmPRwn9tfL4,574
|
|
23
|
+
eeroctl/commands/network/advanced.py,sha256=LDpbeVfBkVQsmghpxEaMwnvPpKFT2wo0aQ9enUmHNXU,5647
|
|
24
|
+
eeroctl/commands/network/backup.py,sha256=McvLCX2rlEav3V1V7vl_8sQm8EPoS6pGDErijLiARWE,5756
|
|
25
|
+
eeroctl/commands/network/base.py,sha256=MW8EW9GfcaUtCp5eGlhPm9ov2ZawePopFY1e13Ee2D4,11424
|
|
26
|
+
eeroctl/commands/network/dhcp.py,sha256=A-yYPkdvjCYBMi5kijETgpijwt5PlXT3cVMKLTtGaEY,3755
|
|
27
|
+
eeroctl/commands/network/dns.py,sha256=E2J0CVLI3Dhephlnr9Y_Bp6uEp9R0H_r-PtdEfZmGRw,6247
|
|
28
|
+
eeroctl/commands/network/forwards.py,sha256=SnzxuL5nlraJmofWMhQIGI7VzBSj3aRLrZ_eGrkPuMo,3680
|
|
29
|
+
eeroctl/commands/network/guest.py,sha256=eYhqWEBGXiyG8HnRGTSCZujwzMU61vRVVcD6c5XxC8U,5168
|
|
30
|
+
eeroctl/commands/network/security.py,sha256=BZQEfyUpJv6GhrW6j6ZWCt94tmH5Z4Lpu-Pvib9tCqA,5506
|
|
31
|
+
eeroctl/commands/network/speedtest.py,sha256=9lxpE6LT0hKDGdAfCs9LB05GNEKEyF_HPhfwl1FqiDA,3309
|
|
32
|
+
eeroctl/commands/network/sqm.py,sha256=l0DyZMsFKi-Jd_qqx1dxkE23pwJR-KJyoXZePNzuZhk,6223
|
|
33
|
+
eeroctl/formatting/__init__.py,sha256=7SxZZyCzIjrfJr-XRasnTmqSKuyHTbmCbRE3XVJVHUk,1830
|
|
34
|
+
eeroctl/formatting/base.py,sha256=__VMPz__etiHYKCdbegEyQyQu2FomsAF6DVEWIZI-hk,5011
|
|
35
|
+
eeroctl/formatting/device.py,sha256=mD8hePr8je69xt4S_AphrKLLEZMvAQGr4QX8g0yqnZY,14322
|
|
36
|
+
eeroctl/formatting/eero.py,sha256=4FShOgQhHNSIZ_OpjjA8WCnmXeIAvUOuhuaHCcz-iOk,19294
|
|
37
|
+
eeroctl/formatting/misc.py,sha256=gG7t9PXkMqtDLJvw4QvQull7iS3XeI5MYPKYJHr0FjY,2688
|
|
38
|
+
eeroctl/formatting/network.py,sha256=iyqIkLgmWTL_boj4yg0H10WrqZSGtKmUAU0yfRILNBE,22663
|
|
39
|
+
eeroctl/formatting/profile.py,sha256=FogqLzNkZP4SHqSyVrG7Q9B0UPeKVl2pvGJWpmgsM7U,15012
|
|
40
|
+
eeroctl-1.7.1.dist-info/licenses/LICENSE,sha256=VL7wy_qH5snbBmKamgHHb6PYrAmk4tcj0R_MVBA3FlU,1078
|
|
41
|
+
eeroctl-1.7.1.dist-info/METADATA,sha256=UdlU9FoN03uDPH-DTN9cZqOPNkjckcJhVYgl8Y9rz5Q,3644
|
|
42
|
+
eeroctl-1.7.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
43
|
+
eeroctl-1.7.1.dist-info/entry_points.txt,sha256=1GTBJ02E21uwOP1frYBhMjx8zmIk1Sf8OFuI3YFK4Lk,69
|
|
44
|
+
eeroctl-1.7.1.dist-info/top_level.txt,sha256=9H9_4a997qOKPPivIK7pHbo7Gk9vZ3WU4VrKPZCBdHs,8
|
|
45
|
+
eeroctl-1.7.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Eero CLI Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
eeroctl
|