ui-cli 1.2.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.
Files changed (46) hide show
  1. ui_cli/__init__.py +31 -0
  2. ui_cli/client.py +269 -0
  3. ui_cli/commands/__init__.py +1 -0
  4. ui_cli/commands/devices.py +187 -0
  5. ui_cli/commands/groups.py +503 -0
  6. ui_cli/commands/hosts.py +114 -0
  7. ui_cli/commands/isp.py +100 -0
  8. ui_cli/commands/local/__init__.py +63 -0
  9. ui_cli/commands/local/apgroups.py +445 -0
  10. ui_cli/commands/local/clients.py +1537 -0
  11. ui_cli/commands/local/config.py +758 -0
  12. ui_cli/commands/local/devices.py +570 -0
  13. ui_cli/commands/local/dpi.py +369 -0
  14. ui_cli/commands/local/events.py +289 -0
  15. ui_cli/commands/local/firewall.py +285 -0
  16. ui_cli/commands/local/health.py +195 -0
  17. ui_cli/commands/local/networks.py +426 -0
  18. ui_cli/commands/local/portfwd.py +153 -0
  19. ui_cli/commands/local/stats.py +234 -0
  20. ui_cli/commands/local/utils.py +85 -0
  21. ui_cli/commands/local/vouchers.py +410 -0
  22. ui_cli/commands/local/wan.py +302 -0
  23. ui_cli/commands/local/wlans.py +257 -0
  24. ui_cli/commands/mcp.py +416 -0
  25. ui_cli/commands/sdwan.py +168 -0
  26. ui_cli/commands/sites.py +65 -0
  27. ui_cli/commands/speedtest.py +192 -0
  28. ui_cli/commands/status.py +410 -0
  29. ui_cli/commands/version.py +13 -0
  30. ui_cli/config.py +106 -0
  31. ui_cli/groups.py +567 -0
  32. ui_cli/local_client.py +897 -0
  33. ui_cli/main.py +61 -0
  34. ui_cli/models.py +188 -0
  35. ui_cli/output.py +251 -0
  36. ui_cli-1.2.1.dist-info/METADATA +1315 -0
  37. ui_cli-1.2.1.dist-info/RECORD +46 -0
  38. ui_cli-1.2.1.dist-info/WHEEL +4 -0
  39. ui_cli-1.2.1.dist-info/entry_points.txt +3 -0
  40. ui_cli-1.2.1.dist-info/licenses/LICENSE +21 -0
  41. ui_mcp/ARCHITECTURE.md +243 -0
  42. ui_mcp/README.md +235 -0
  43. ui_mcp/__init__.py +7 -0
  44. ui_mcp/__main__.py +10 -0
  45. ui_mcp/cli_runner.py +112 -0
  46. ui_mcp/server.py +468 -0
@@ -0,0 +1,168 @@
1
+ """SD-WAN configuration commands."""
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from ui_cli.client import APIError, UniFiClient
9
+ from ui_cli.output import OutputFormat, print_error, render_output
10
+
11
+ app = typer.Typer(help="Manage SD-WAN configurations")
12
+
13
+ # Column definitions for SD-WAN configs table
14
+ SDWAN_COLUMNS = [
15
+ ("id", "ID"),
16
+ ("name", "Name"),
17
+ ("type", "Type"),
18
+ ("variant", "Variant"),
19
+ ]
20
+
21
+ # Column definitions for SD-WAN status table
22
+ SDWAN_STATUS_COLUMNS = [
23
+ ("fingerprint", "Fingerprint"),
24
+ ("status", "Status"),
25
+ ("progress", "Progress"),
26
+ ("updatedAt", "Updated At"),
27
+ ]
28
+
29
+
30
+ @app.command("list")
31
+ def list_configs(
32
+ output: Annotated[
33
+ OutputFormat,
34
+ typer.Option(
35
+ "--output",
36
+ "-o",
37
+ help="Output format: table, json, or csv",
38
+ ),
39
+ ] = OutputFormat.TABLE,
40
+ verbose: Annotated[
41
+ bool,
42
+ typer.Option(
43
+ "--verbose",
44
+ "-v",
45
+ help="Show detailed request/response information",
46
+ ),
47
+ ] = False,
48
+ ) -> None:
49
+ """List all SD-WAN configurations."""
50
+
51
+ async def _list() -> list:
52
+ client = UniFiClient()
53
+ return await client.list_sdwan_configs()
54
+
55
+ try:
56
+ configs = asyncio.run(_list())
57
+
58
+ if verbose:
59
+ typer.echo(f"Found {len(configs)} SD-WAN configuration(s)")
60
+
61
+ render_output(
62
+ data=configs,
63
+ output_format=output,
64
+ columns=SDWAN_COLUMNS,
65
+ title="SD-WAN Configurations",
66
+ verbose=verbose,
67
+ )
68
+ except APIError as e:
69
+ print_error(e.message)
70
+ raise typer.Exit(1)
71
+
72
+
73
+ @app.command("get")
74
+ def get_config(
75
+ config_id: Annotated[
76
+ str,
77
+ typer.Argument(help="The unique identifier of the SD-WAN config"),
78
+ ],
79
+ output: Annotated[
80
+ OutputFormat,
81
+ typer.Option(
82
+ "--output",
83
+ "-o",
84
+ help="Output format: table, json, or csv",
85
+ ),
86
+ ] = OutputFormat.TABLE,
87
+ verbose: Annotated[
88
+ bool,
89
+ typer.Option(
90
+ "--verbose",
91
+ "-v",
92
+ help="Show detailed request/response information",
93
+ ),
94
+ ] = False,
95
+ ) -> None:
96
+ """Get detailed information about a specific SD-WAN configuration."""
97
+
98
+ async def _get() -> dict:
99
+ client = UniFiClient()
100
+ return await client.get_sdwan_config(config_id)
101
+
102
+ try:
103
+ config = asyncio.run(_get())
104
+
105
+ if not config:
106
+ print_error(f"SD-WAN config '{config_id}' not found")
107
+ raise typer.Exit(1)
108
+
109
+ render_output(
110
+ data=config,
111
+ output_format=output,
112
+ columns=SDWAN_COLUMNS,
113
+ title=f"SD-WAN Config: {config_id}",
114
+ verbose=verbose,
115
+ is_single=True,
116
+ )
117
+ except APIError as e:
118
+ print_error(e.message)
119
+ raise typer.Exit(1)
120
+
121
+
122
+ @app.command("status")
123
+ def get_status(
124
+ config_id: Annotated[
125
+ str,
126
+ typer.Argument(help="The unique identifier of the SD-WAN config"),
127
+ ],
128
+ output: Annotated[
129
+ OutputFormat,
130
+ typer.Option(
131
+ "--output",
132
+ "-o",
133
+ help="Output format: table, json, or csv",
134
+ ),
135
+ ] = OutputFormat.TABLE,
136
+ verbose: Annotated[
137
+ bool,
138
+ typer.Option(
139
+ "--verbose",
140
+ "-v",
141
+ help="Show detailed request/response information",
142
+ ),
143
+ ] = False,
144
+ ) -> None:
145
+ """Get deployment status of a specific SD-WAN configuration."""
146
+
147
+ async def _get() -> dict:
148
+ client = UniFiClient()
149
+ return await client.get_sdwan_status(config_id)
150
+
151
+ try:
152
+ status = asyncio.run(_get())
153
+
154
+ if not status:
155
+ print_error(f"Status for SD-WAN config '{config_id}' not found")
156
+ raise typer.Exit(1)
157
+
158
+ render_output(
159
+ data=status,
160
+ output_format=output,
161
+ columns=SDWAN_STATUS_COLUMNS,
162
+ title=f"SD-WAN Status: {config_id}",
163
+ verbose=verbose,
164
+ is_single=True,
165
+ )
166
+ except APIError as e:
167
+ print_error(e.message)
168
+ raise typer.Exit(1)
@@ -0,0 +1,65 @@
1
+ """Site management commands."""
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from ui_cli.client import APIError, UniFiClient
9
+ from ui_cli.output import OutputFormat, print_error, render_output
10
+
11
+ app = typer.Typer(help="Manage UniFi sites")
12
+
13
+ # Column definitions for sites table
14
+ SITE_COLUMNS = [
15
+ ("siteId", "Site ID"),
16
+ ("meta.name", "Name"),
17
+ ("meta.desc", "Description"),
18
+ ("hostId", "Host ID"),
19
+ ("meta.timezone", "Timezone"),
20
+ ("permission", "Permission"),
21
+ ("isOwner", "Owner"),
22
+ ]
23
+
24
+
25
+ @app.command("list")
26
+ def list_sites(
27
+ output: Annotated[
28
+ OutputFormat,
29
+ typer.Option(
30
+ "--output",
31
+ "-o",
32
+ help="Output format: table, json, or csv",
33
+ ),
34
+ ] = OutputFormat.TABLE,
35
+ verbose: Annotated[
36
+ bool,
37
+ typer.Option(
38
+ "--verbose",
39
+ "-v",
40
+ help="Show detailed request/response information",
41
+ ),
42
+ ] = False,
43
+ ) -> None:
44
+ """List all sites from hosts running UniFi Network application."""
45
+
46
+ async def _list() -> list:
47
+ client = UniFiClient()
48
+ return await client.list_sites()
49
+
50
+ try:
51
+ sites = asyncio.run(_list())
52
+
53
+ if verbose:
54
+ typer.echo(f"Found {len(sites)} site(s)")
55
+
56
+ render_output(
57
+ data=sites,
58
+ output_format=output,
59
+ columns=SITE_COLUMNS,
60
+ title="UniFi Sites",
61
+ verbose=verbose,
62
+ )
63
+ except APIError as e:
64
+ print_error(e.message)
65
+ raise typer.Exit(1)
@@ -0,0 +1,192 @@
1
+ """Speed test command - run speed test on gateway."""
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from ui_cli.local_client import LocalAPIError, UniFiLocalClient
10
+ from ui_cli.output import OutputFormat, console, output_json, print_error
11
+
12
+ app = typer.Typer(help="Run speed test on gateway", invoke_without_command=True)
13
+
14
+
15
+ async def run_speedtest(client: UniFiLocalClient) -> dict:
16
+ """Trigger speed test and wait for results."""
17
+ # Trigger the speed test
18
+ response = await client.post("/cmd/devmgr", data={"cmd": "speedtest"})
19
+
20
+ if response.get("meta", {}).get("rc") != "ok":
21
+ raise LocalAPIError("Failed to start speed test")
22
+
23
+ return response
24
+
25
+
26
+ async def get_speedtest_status(client: UniFiLocalClient) -> dict | None:
27
+ """Get current speed test status."""
28
+ response = await client.post("/cmd/devmgr", data={"cmd": "speedtest-status"})
29
+ data = response.get("data", [])
30
+ return data[0] if data else None
31
+
32
+
33
+ async def get_latest_speedtest(client: UniFiLocalClient) -> dict | None:
34
+ """Get most recent speed test result from health endpoint."""
35
+ # Speed test data is in the www (Internet) subsystem of health
36
+ response = await client.get("/stat/health")
37
+ data = response.get("data", [])
38
+
39
+ for subsystem in data:
40
+ if subsystem.get("subsystem") == "www":
41
+ return subsystem
42
+
43
+ return None
44
+
45
+
46
+ def format_speed(bps: float | None) -> str:
47
+ """Format speed in appropriate units."""
48
+ if bps is None or bps == 0:
49
+ return "-"
50
+
51
+ mbps = bps / 1_000_000
52
+ if mbps >= 1000:
53
+ return f"{mbps / 1000:.1f} Gbps"
54
+ elif mbps >= 1:
55
+ return f"{mbps:.1f} Mbps"
56
+ else:
57
+ kbps = bps / 1000
58
+ return f"{kbps:.0f} Kbps"
59
+
60
+
61
+ def format_latency(ms: float | None) -> str:
62
+ """Format latency."""
63
+ if ms is None:
64
+ return "-"
65
+ return f"{ms:.0f} ms"
66
+
67
+
68
+ @app.callback(invoke_without_command=True)
69
+ def speedtest(
70
+ ctx: typer.Context,
71
+ run: Annotated[
72
+ bool,
73
+ typer.Option("--run", "-r", help="Run a new speed test"),
74
+ ] = False,
75
+ output: Annotated[
76
+ OutputFormat,
77
+ typer.Option("--output", "-o", help="Output format"),
78
+ ] = OutputFormat.TABLE,
79
+ ) -> None:
80
+ """Show last speed test result or run a new test."""
81
+ if ctx.invoked_subcommand is not None:
82
+ return
83
+
84
+ async def _speedtest():
85
+ client = UniFiLocalClient()
86
+
87
+ if run:
88
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
89
+
90
+ # Run a new speed test
91
+ await run_speedtest(client)
92
+
93
+ # Poll for completion using health endpoint with progress bar
94
+ max_wait = 90 # seconds (speed tests can take a while)
95
+ start = time.time()
96
+
97
+ with Progress(
98
+ SpinnerColumn(),
99
+ TextColumn("[cyan]Running speed test..."),
100
+ BarColumn(bar_width=30),
101
+ TimeElapsedColumn(),
102
+ console=console,
103
+ transient=True,
104
+ ) as progress:
105
+ task = progress.add_task("speedtest", total=max_wait)
106
+
107
+ while time.time() - start < max_wait:
108
+ await asyncio.sleep(2)
109
+ elapsed = time.time() - start
110
+ progress.update(task, completed=min(elapsed, max_wait))
111
+
112
+ # Check www subsystem for speedtest_status
113
+ health = await client.get("/stat/health")
114
+ for subsystem in health.get("data", []):
115
+ if subsystem.get("subsystem") == "www":
116
+ status = subsystem.get("speedtest_status", "")
117
+ if status.lower() != "running":
118
+ progress.update(task, completed=max_wait)
119
+ break
120
+ else:
121
+ continue
122
+ break
123
+
124
+ # Get the latest result
125
+ return await get_latest_speedtest(client)
126
+
127
+ try:
128
+ result = asyncio.run(_speedtest())
129
+ except LocalAPIError as e:
130
+ print_error(str(e))
131
+ raise typer.Exit(1)
132
+
133
+ if not result:
134
+ console.print("[dim]No speed test results found. Run with --run to start a test.[/dim]")
135
+ return
136
+
137
+ if output == OutputFormat.JSON:
138
+ output_json(result)
139
+ return
140
+
141
+ # Table output
142
+ from datetime import datetime, timezone
143
+ from rich.table import Table
144
+
145
+ console.print()
146
+ console.print("[bold cyan]Speed Test Results[/bold cyan]")
147
+ console.print("─" * 40)
148
+ console.print()
149
+
150
+ # Format timestamp (speedtest_lastrun is in seconds)
151
+ ts = result.get("speedtest_lastrun", 0)
152
+ if ts:
153
+ dt = datetime.fromtimestamp(ts, tz=timezone.utc)
154
+ time_str = dt.strftime("%Y-%m-%d %H:%M:%S UTC")
155
+ else:
156
+ time_str = "-"
157
+
158
+ table = Table(show_header=False, box=None, padding=(0, 2))
159
+ table.add_column("Key", style="dim")
160
+ table.add_column("Value")
161
+
162
+ # Status
163
+ status = result.get("speedtest_status", "")
164
+ if status:
165
+ if status.lower() == "running":
166
+ table.add_row("Status:", f"[yellow]{status}[/yellow]")
167
+ else:
168
+ table.add_row("Status:", f"[green]{status}[/green]")
169
+
170
+ table.add_row("Last Run:", time_str)
171
+ table.add_row("", "")
172
+
173
+ # Download (xput_down is in Mbps)
174
+ download = result.get("xput_down", 0)
175
+ if download:
176
+ table.add_row("Download:", f"[green]{download} Mbps[/green]")
177
+ else:
178
+ table.add_row("Download:", "-")
179
+
180
+ # Upload (xput_up is in Mbps)
181
+ upload = result.get("xput_up", 0)
182
+ if upload:
183
+ table.add_row("Upload:", f"[blue]{upload} Mbps[/blue]")
184
+ else:
185
+ table.add_row("Upload:", "-")
186
+
187
+ # Latency
188
+ latency = result.get("speedtest_ping", result.get("latency", 0))
189
+ table.add_row("Latency:", format_latency(latency))
190
+
191
+ console.print(table)
192
+ console.print()