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.
- ui_cli/__init__.py +31 -0
- ui_cli/client.py +269 -0
- ui_cli/commands/__init__.py +1 -0
- ui_cli/commands/devices.py +187 -0
- ui_cli/commands/groups.py +503 -0
- ui_cli/commands/hosts.py +114 -0
- ui_cli/commands/isp.py +100 -0
- ui_cli/commands/local/__init__.py +63 -0
- ui_cli/commands/local/apgroups.py +445 -0
- ui_cli/commands/local/clients.py +1537 -0
- ui_cli/commands/local/config.py +758 -0
- ui_cli/commands/local/devices.py +570 -0
- ui_cli/commands/local/dpi.py +369 -0
- ui_cli/commands/local/events.py +289 -0
- ui_cli/commands/local/firewall.py +285 -0
- ui_cli/commands/local/health.py +195 -0
- ui_cli/commands/local/networks.py +426 -0
- ui_cli/commands/local/portfwd.py +153 -0
- ui_cli/commands/local/stats.py +234 -0
- ui_cli/commands/local/utils.py +85 -0
- ui_cli/commands/local/vouchers.py +410 -0
- ui_cli/commands/local/wan.py +302 -0
- ui_cli/commands/local/wlans.py +257 -0
- ui_cli/commands/mcp.py +416 -0
- ui_cli/commands/sdwan.py +168 -0
- ui_cli/commands/sites.py +65 -0
- ui_cli/commands/speedtest.py +192 -0
- ui_cli/commands/status.py +410 -0
- ui_cli/commands/version.py +13 -0
- ui_cli/config.py +106 -0
- ui_cli/groups.py +567 -0
- ui_cli/local_client.py +897 -0
- ui_cli/main.py +61 -0
- ui_cli/models.py +188 -0
- ui_cli/output.py +251 -0
- ui_cli-1.2.1.dist-info/METADATA +1315 -0
- ui_cli-1.2.1.dist-info/RECORD +46 -0
- ui_cli-1.2.1.dist-info/WHEEL +4 -0
- ui_cli-1.2.1.dist-info/entry_points.txt +3 -0
- ui_cli-1.2.1.dist-info/licenses/LICENSE +21 -0
- ui_mcp/ARCHITECTURE.md +243 -0
- ui_mcp/README.md +235 -0
- ui_mcp/__init__.py +7 -0
- ui_mcp/__main__.py +10 -0
- ui_mcp/cli_runner.py +112 -0
- ui_mcp/server.py +468 -0
ui_cli/main.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""UniFi Site Manager CLI - Main entry point."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ui_cli import __version__
|
|
6
|
+
from ui_cli.commands import devices, groups, hosts, isp, mcp, sdwan, sites, speedtest, status, version
|
|
7
|
+
from ui_cli.commands import local
|
|
8
|
+
|
|
9
|
+
# Create main app
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="ui",
|
|
12
|
+
help="UniFi Site Manager CLI - Manage your UniFi infrastructure from the command line.",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
add_completion=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Register command groups
|
|
18
|
+
app.add_typer(status.app, name="status")
|
|
19
|
+
app.add_typer(hosts.app, name="hosts")
|
|
20
|
+
app.add_typer(sites.app, name="sites")
|
|
21
|
+
app.add_typer(devices.app, name="devices")
|
|
22
|
+
app.add_typer(isp.app, name="isp")
|
|
23
|
+
app.add_typer(sdwan.app, name="sdwan")
|
|
24
|
+
app.add_typer(version.app, name="version")
|
|
25
|
+
app.add_typer(speedtest.app, name="speedtest")
|
|
26
|
+
|
|
27
|
+
# Local controller commands (with alias)
|
|
28
|
+
app.add_typer(local.app, name="local")
|
|
29
|
+
app.add_typer(local.app, name="lo")
|
|
30
|
+
|
|
31
|
+
# MCP server management
|
|
32
|
+
app.add_typer(mcp.app, name="mcp")
|
|
33
|
+
|
|
34
|
+
# Client groups (local storage, no controller needed)
|
|
35
|
+
app.add_typer(groups.app, name="groups")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def version_callback(value: bool) -> None:
|
|
39
|
+
"""Print version and exit."""
|
|
40
|
+
if value:
|
|
41
|
+
typer.echo(f"ui-cli version {__version__}")
|
|
42
|
+
raise typer.Exit()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.callback()
|
|
46
|
+
def main(
|
|
47
|
+
version: bool = typer.Option(
|
|
48
|
+
None,
|
|
49
|
+
"--version",
|
|
50
|
+
"-V",
|
|
51
|
+
callback=version_callback,
|
|
52
|
+
is_eager=True,
|
|
53
|
+
help="Show version and exit",
|
|
54
|
+
),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""UniFi Site Manager CLI - Manage your UniFi infrastructure from the command line."""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
app()
|
ui_cli/models.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Pydantic models for UniFi API responses."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ========== Host Models ==========
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HostReportedState(BaseModel):
|
|
13
|
+
"""Host reported state information."""
|
|
14
|
+
|
|
15
|
+
hostname: str | None = None
|
|
16
|
+
version: str | None = None
|
|
17
|
+
hardware_id: str | None = Field(None, alias="hardwareId")
|
|
18
|
+
firmware_version: str | None = Field(None, alias="firmwareVersion")
|
|
19
|
+
ip_address: str | None = Field(None, alias="ipAddress")
|
|
20
|
+
mac_address: str | None = Field(None, alias="macAddress")
|
|
21
|
+
|
|
22
|
+
class Config:
|
|
23
|
+
populate_by_name = True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Host(BaseModel):
|
|
27
|
+
"""UniFi host (console/controller) model."""
|
|
28
|
+
|
|
29
|
+
id: str
|
|
30
|
+
hardware_id: str | None = Field(None, alias="hardwareId")
|
|
31
|
+
type: str | None = None
|
|
32
|
+
ip_address: str | None = Field(None, alias="ipAddress")
|
|
33
|
+
is_blocked: bool | None = Field(None, alias="isBlocked")
|
|
34
|
+
last_connection_state_change: datetime | None = Field(
|
|
35
|
+
None, alias="lastConnectionStateChange"
|
|
36
|
+
)
|
|
37
|
+
latest_backup_time: datetime | None = Field(None, alias="latestBackupTime")
|
|
38
|
+
registration_time: datetime | None = Field(None, alias="registrationTime")
|
|
39
|
+
owner: bool | None = None
|
|
40
|
+
reported_state: HostReportedState | None = Field(None, alias="reportedState")
|
|
41
|
+
user_data: dict[str, Any] | None = Field(None, alias="userData")
|
|
42
|
+
|
|
43
|
+
class Config:
|
|
44
|
+
populate_by_name = True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ========== Site Models ==========
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SiteMeta(BaseModel):
|
|
51
|
+
"""Site metadata."""
|
|
52
|
+
|
|
53
|
+
name: str | None = None
|
|
54
|
+
desc: str | None = None
|
|
55
|
+
timezone: str | None = None
|
|
56
|
+
gateway_mac: str | None = Field(None, alias="gatewayMac")
|
|
57
|
+
|
|
58
|
+
class Config:
|
|
59
|
+
populate_by_name = True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SiteStatistics(BaseModel):
|
|
63
|
+
"""Site statistics."""
|
|
64
|
+
|
|
65
|
+
counts: dict[str, int] | None = None
|
|
66
|
+
|
|
67
|
+
class Config:
|
|
68
|
+
populate_by_name = True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Site(BaseModel):
|
|
72
|
+
"""UniFi site model."""
|
|
73
|
+
|
|
74
|
+
site_id: str | None = Field(None, alias="siteId")
|
|
75
|
+
host_id: str | None = Field(None, alias="hostId")
|
|
76
|
+
is_owner: bool | None = Field(None, alias="isOwner")
|
|
77
|
+
permission: str | None = None
|
|
78
|
+
meta: SiteMeta | None = None
|
|
79
|
+
statistics: SiteStatistics | None = None
|
|
80
|
+
subscription_end_time: datetime | None = Field(None, alias="subscriptionEndTime")
|
|
81
|
+
|
|
82
|
+
class Config:
|
|
83
|
+
populate_by_name = True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ========== Device Models ==========
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DeviceUidb(BaseModel):
|
|
90
|
+
"""Device UIDB (database) information."""
|
|
91
|
+
|
|
92
|
+
id: str | None = None
|
|
93
|
+
guid: str | None = None
|
|
94
|
+
images: dict[str, Any] | None = None
|
|
95
|
+
|
|
96
|
+
class Config:
|
|
97
|
+
populate_by_name = True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Device(BaseModel):
|
|
101
|
+
"""UniFi device model."""
|
|
102
|
+
|
|
103
|
+
id: str
|
|
104
|
+
mac: str | None = None
|
|
105
|
+
name: str | None = None
|
|
106
|
+
model: str | None = None
|
|
107
|
+
shortname: str | None = None
|
|
108
|
+
ip: str | None = None
|
|
109
|
+
product_line: str | None = Field(None, alias="productLine")
|
|
110
|
+
status: str | None = None
|
|
111
|
+
version: str | None = None
|
|
112
|
+
firmware_status: str | None = Field(None, alias="firmwareStatus")
|
|
113
|
+
is_console: bool | None = Field(None, alias="isConsole")
|
|
114
|
+
is_managed: bool | None = Field(None, alias="isManaged")
|
|
115
|
+
startup_time: datetime | None = Field(None, alias="startupTime")
|
|
116
|
+
adoption_time: datetime | None = Field(None, alias="adoptionTime")
|
|
117
|
+
host_id: str | None = Field(None, alias="hostId")
|
|
118
|
+
host_name: str | None = Field(None, alias="hostName")
|
|
119
|
+
updated_at: datetime | None = Field(None, alias="updatedAt")
|
|
120
|
+
uidb: DeviceUidb | None = None
|
|
121
|
+
|
|
122
|
+
class Config:
|
|
123
|
+
populate_by_name = True
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ========== ISP Metrics Models ==========
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ISPMetric(BaseModel):
|
|
130
|
+
"""ISP performance metric model."""
|
|
131
|
+
|
|
132
|
+
site_id: str | None = Field(None, alias="siteId")
|
|
133
|
+
host_id: str | None = Field(None, alias="hostId")
|
|
134
|
+
timestamp: datetime | None = None
|
|
135
|
+
avg_latency: float | None = Field(None, alias="avgLatency")
|
|
136
|
+
max_latency: float | None = Field(None, alias="maxLatency")
|
|
137
|
+
download_kbps: float | None = Field(None, alias="downloadKbps")
|
|
138
|
+
upload_kbps: float | None = Field(None, alias="uploadKbps")
|
|
139
|
+
uptime: float | None = None
|
|
140
|
+
downtime: float | None = None
|
|
141
|
+
packet_loss: float | None = Field(None, alias="packetLoss")
|
|
142
|
+
isp_name: str | None = Field(None, alias="ispName")
|
|
143
|
+
isp_asn: str | None = Field(None, alias="ispAsn")
|
|
144
|
+
|
|
145
|
+
class Config:
|
|
146
|
+
populate_by_name = True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ========== SD-WAN Models ==========
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SDWanSettings(BaseModel):
|
|
153
|
+
"""SD-WAN configuration settings."""
|
|
154
|
+
|
|
155
|
+
hubs_interconnect: bool | None = Field(None, alias="hubsInterconnect")
|
|
156
|
+
spoke_to_hub_tunnels_mode: str | None = Field(None, alias="spokeToHubTunnelsMode")
|
|
157
|
+
spokes_auto_scale_and_nat_enabled: bool | None = Field(
|
|
158
|
+
None, alias="spokesAutoScaleAndNatEnabled"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
class Config:
|
|
162
|
+
populate_by_name = True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class SDWanConfig(BaseModel):
|
|
166
|
+
"""SD-WAN configuration model."""
|
|
167
|
+
|
|
168
|
+
id: str
|
|
169
|
+
name: str | None = None
|
|
170
|
+
type: str | None = None
|
|
171
|
+
variant: str | None = None
|
|
172
|
+
settings: SDWanSettings | None = None
|
|
173
|
+
|
|
174
|
+
class Config:
|
|
175
|
+
populate_by_name = True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SDWanStatus(BaseModel):
|
|
179
|
+
"""SD-WAN deployment status model."""
|
|
180
|
+
|
|
181
|
+
fingerprint: str | None = None
|
|
182
|
+
updated_at: datetime | None = Field(None, alias="updatedAt")
|
|
183
|
+
status: str | None = None
|
|
184
|
+
progress: float | None = None
|
|
185
|
+
errors: list[str] | None = None
|
|
186
|
+
|
|
187
|
+
class Config:
|
|
188
|
+
populate_by_name = True
|
ui_cli/output.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Output formatters for table, JSON, and CSV formats."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.json import JSON
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OutputFormat(str, Enum):
|
|
17
|
+
"""Available output formats."""
|
|
18
|
+
|
|
19
|
+
TABLE = "table"
|
|
20
|
+
JSON = "json"
|
|
21
|
+
CSV = "csv"
|
|
22
|
+
YAML = "yaml"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def flatten_dict(data: dict[str, Any], parent_key: str = "", sep: str = ".") -> dict[str, Any]:
|
|
26
|
+
"""Flatten nested dictionary for CSV output."""
|
|
27
|
+
items: list[tuple[str, Any]] = []
|
|
28
|
+
for key, value in data.items():
|
|
29
|
+
new_key = f"{parent_key}{sep}{key}" if parent_key else key
|
|
30
|
+
if isinstance(value, dict):
|
|
31
|
+
items.extend(flatten_dict(value, new_key, sep=sep).items())
|
|
32
|
+
elif isinstance(value, list):
|
|
33
|
+
items.append((new_key, json.dumps(value)))
|
|
34
|
+
else:
|
|
35
|
+
items.append((new_key, value))
|
|
36
|
+
return dict(items)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def output_json(data: Any, verbose: bool = False) -> None:
|
|
40
|
+
"""Output data as formatted JSON."""
|
|
41
|
+
if verbose:
|
|
42
|
+
console.print(JSON(json.dumps(data, indent=2, default=str)))
|
|
43
|
+
else:
|
|
44
|
+
print(json.dumps(data, indent=2, default=str))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_nested_value(data: dict[str, Any], key: str) -> Any:
|
|
48
|
+
"""Get value from nested dict using dot notation key."""
|
|
49
|
+
value = data
|
|
50
|
+
for part in key.split("."):
|
|
51
|
+
if isinstance(value, dict):
|
|
52
|
+
value = value.get(part, "")
|
|
53
|
+
else:
|
|
54
|
+
return ""
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def output_csv(
|
|
59
|
+
data: list[dict[str, Any]],
|
|
60
|
+
columns: list[tuple[str, str]] | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Output data as CSV.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
data: List of dictionaries to output
|
|
66
|
+
columns: Optional list of (key, header) tuples. If None, flattens all fields.
|
|
67
|
+
"""
|
|
68
|
+
if not data:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
output = io.StringIO()
|
|
72
|
+
|
|
73
|
+
if columns:
|
|
74
|
+
# Use specified columns with headers
|
|
75
|
+
headers = [header for _, header in columns]
|
|
76
|
+
writer = csv.writer(output)
|
|
77
|
+
writer.writerow(headers)
|
|
78
|
+
|
|
79
|
+
for item in data:
|
|
80
|
+
row = []
|
|
81
|
+
for key, _ in columns:
|
|
82
|
+
value = get_nested_value(item, key)
|
|
83
|
+
if value is None:
|
|
84
|
+
value = ""
|
|
85
|
+
elif isinstance(value, bool):
|
|
86
|
+
value = "Yes" if value else "No"
|
|
87
|
+
elif isinstance(value, (list, dict)):
|
|
88
|
+
value = json.dumps(value)
|
|
89
|
+
else:
|
|
90
|
+
value = str(value)
|
|
91
|
+
row.append(value)
|
|
92
|
+
writer.writerow(row)
|
|
93
|
+
else:
|
|
94
|
+
# Flatten and output all fields
|
|
95
|
+
flattened = [flatten_dict(item) for item in data]
|
|
96
|
+
all_keys: set[str] = set()
|
|
97
|
+
for item in flattened:
|
|
98
|
+
all_keys.update(item.keys())
|
|
99
|
+
fieldnames = sorted(all_keys)
|
|
100
|
+
|
|
101
|
+
writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore")
|
|
102
|
+
writer.writeheader()
|
|
103
|
+
for item in flattened:
|
|
104
|
+
writer.writerow(item)
|
|
105
|
+
|
|
106
|
+
print(output.getvalue(), end="")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def output_table(
|
|
110
|
+
data: list[dict[str, Any]],
|
|
111
|
+
columns: list[tuple[str, str]],
|
|
112
|
+
title: str | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Output data as a Rich table.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
data: List of dictionaries to display
|
|
118
|
+
columns: List of (key, header) tuples defining columns
|
|
119
|
+
title: Optional table title
|
|
120
|
+
"""
|
|
121
|
+
table = Table(title=title, show_header=True, header_style="bold cyan")
|
|
122
|
+
|
|
123
|
+
# Add columns
|
|
124
|
+
for _, header in columns:
|
|
125
|
+
table.add_column(header)
|
|
126
|
+
|
|
127
|
+
# Add rows
|
|
128
|
+
for item in data:
|
|
129
|
+
row = []
|
|
130
|
+
for key, _ in columns:
|
|
131
|
+
value = item
|
|
132
|
+
# Handle nested keys like "meta.name"
|
|
133
|
+
for part in key.split("."):
|
|
134
|
+
if isinstance(value, dict):
|
|
135
|
+
value = value.get(part, "")
|
|
136
|
+
else:
|
|
137
|
+
value = ""
|
|
138
|
+
break
|
|
139
|
+
# Format value
|
|
140
|
+
if value is None:
|
|
141
|
+
value = ""
|
|
142
|
+
elif isinstance(value, bool):
|
|
143
|
+
value = "Yes" if value else "No"
|
|
144
|
+
elif isinstance(value, (list, dict)):
|
|
145
|
+
value = json.dumps(value)
|
|
146
|
+
else:
|
|
147
|
+
value = str(value)
|
|
148
|
+
row.append(value)
|
|
149
|
+
table.add_row(*row)
|
|
150
|
+
|
|
151
|
+
console.print(table)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def output_single_table(
|
|
155
|
+
data: dict[str, Any],
|
|
156
|
+
title: str | None = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Output a single item as a key-value table."""
|
|
159
|
+
table = Table(title=title, show_header=True, header_style="bold cyan")
|
|
160
|
+
table.add_column("Field", style="cyan")
|
|
161
|
+
table.add_column("Value")
|
|
162
|
+
|
|
163
|
+
flat = flatten_dict(data)
|
|
164
|
+
for key, value in flat.items():
|
|
165
|
+
if value is None:
|
|
166
|
+
value = ""
|
|
167
|
+
elif isinstance(value, bool):
|
|
168
|
+
value = "Yes" if value else "No"
|
|
169
|
+
else:
|
|
170
|
+
value = str(value)
|
|
171
|
+
table.add_row(key, value)
|
|
172
|
+
|
|
173
|
+
console.print(table)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def output_count_table(
|
|
177
|
+
counts: dict[str, int],
|
|
178
|
+
group_header: str = "Group",
|
|
179
|
+
count_header: str = "Count",
|
|
180
|
+
title: str | None = None,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Output count data as a table with totals."""
|
|
183
|
+
table = Table(title=title, show_header=True, header_style="bold cyan")
|
|
184
|
+
table.add_column(group_header)
|
|
185
|
+
table.add_column(count_header, justify="right")
|
|
186
|
+
|
|
187
|
+
total = 0
|
|
188
|
+
for group, count in sorted(counts.items()):
|
|
189
|
+
table.add_row(group, str(count))
|
|
190
|
+
total += count
|
|
191
|
+
|
|
192
|
+
# Add separator and total
|
|
193
|
+
table.add_row("─" * 20, "─" * 10, style="dim")
|
|
194
|
+
table.add_row("Total", str(total), style="bold")
|
|
195
|
+
|
|
196
|
+
console.print(table)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def render_output(
|
|
200
|
+
data: Any,
|
|
201
|
+
output_format: OutputFormat,
|
|
202
|
+
columns: list[tuple[str, str]] | None = None,
|
|
203
|
+
title: str | None = None,
|
|
204
|
+
verbose: bool = False,
|
|
205
|
+
is_single: bool = False,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Render data in the specified format.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
data: Data to render (list or dict)
|
|
211
|
+
output_format: Output format (table, json, csv)
|
|
212
|
+
columns: Column definitions for table format [(key, header), ...]
|
|
213
|
+
title: Title for table output
|
|
214
|
+
verbose: Enable verbose output
|
|
215
|
+
is_single: If True, render as single item detail view
|
|
216
|
+
"""
|
|
217
|
+
if output_format == OutputFormat.JSON:
|
|
218
|
+
output_json(data, verbose=verbose)
|
|
219
|
+
elif output_format == OutputFormat.CSV:
|
|
220
|
+
if isinstance(data, dict):
|
|
221
|
+
data = [data]
|
|
222
|
+
output_csv(data, columns=columns)
|
|
223
|
+
else: # TABLE
|
|
224
|
+
if is_single and isinstance(data, dict):
|
|
225
|
+
output_single_table(data, title=title)
|
|
226
|
+
elif isinstance(data, list) and columns:
|
|
227
|
+
output_table(data, columns=columns, title=title)
|
|
228
|
+
elif isinstance(data, dict):
|
|
229
|
+
output_single_table(data, title=title)
|
|
230
|
+
else:
|
|
231
|
+
console.print(data)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def print_error(message: str) -> None:
|
|
235
|
+
"""Print an error message."""
|
|
236
|
+
console.print(f"[red]Error:[/red] {message}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def print_warning(message: str) -> None:
|
|
240
|
+
"""Print a warning message."""
|
|
241
|
+
console.print(f"[yellow]Warning:[/yellow] {message}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def print_success(message: str) -> None:
|
|
245
|
+
"""Print a success message."""
|
|
246
|
+
console.print(f"[green]{message}[/green]")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def print_info(message: str) -> None:
|
|
250
|
+
"""Print an info message."""
|
|
251
|
+
console.print(f"[cyan]{message}[/cyan]")
|