pltr-cli 0.1.2__py3-none-any.whl → 0.3.0__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.
- pltr/__main__.py +31 -0
- pltr/cli.py +21 -1
- pltr/commands/admin.py +530 -0
- pltr/commands/alias.py +241 -0
- pltr/commands/completion.py +383 -0
- pltr/commands/dataset.py +20 -3
- pltr/commands/ontology.py +502 -0
- pltr/commands/shell.py +126 -0
- pltr/commands/sql.py +358 -0
- pltr/commands/verify.py +2 -1
- pltr/config/aliases.py +254 -0
- pltr/services/__init__.py +4 -0
- pltr/services/admin.py +314 -0
- pltr/services/ontology.py +442 -0
- pltr/services/sql.py +340 -0
- pltr/utils/alias_resolver.py +56 -0
- pltr/utils/completion.py +178 -0
- pltr/utils/formatting.py +208 -0
- pltr/utils/progress.py +1 -1
- pltr_cli-0.3.0.dist-info/METADATA +280 -0
- pltr_cli-0.3.0.dist-info/RECORD +41 -0
- pltr_cli-0.1.2.dist-info/METADATA +0 -203
- pltr_cli-0.1.2.dist-info/RECORD +0 -28
- {pltr_cli-0.1.2.dist-info → pltr_cli-0.3.0.dist-info}/WHEEL +0 -0
- {pltr_cli-0.1.2.dist-info → pltr_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {pltr_cli-0.1.2.dist-info → pltr_cli-0.3.0.dist-info}/licenses/LICENSE +0 -0
pltr/commands/sql.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQL commands for the pltr CLI.
|
|
3
|
+
Provides commands for executing SQL queries against Foundry datasets.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from ..services.sql import SqlService
|
|
13
|
+
from ..utils.formatting import OutputFormatter
|
|
14
|
+
from ..utils.progress import SpinnerProgressTracker
|
|
15
|
+
from ..utils.completion import (
|
|
16
|
+
complete_profile,
|
|
17
|
+
complete_output_format,
|
|
18
|
+
complete_sql_query,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(name="sql", help="Execute SQL queries against Foundry datasets")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("execute")
|
|
25
|
+
def execute_query(
|
|
26
|
+
query: str = typer.Argument(
|
|
27
|
+
..., help="SQL query to execute", autocompletion=complete_sql_query
|
|
28
|
+
),
|
|
29
|
+
profile: Optional[str] = typer.Option(
|
|
30
|
+
None, "--profile", help="Auth profile to use", autocompletion=complete_profile
|
|
31
|
+
),
|
|
32
|
+
output_format: str = typer.Option(
|
|
33
|
+
"table",
|
|
34
|
+
"--format",
|
|
35
|
+
help="Output format (table, json, csv)",
|
|
36
|
+
autocompletion=complete_output_format,
|
|
37
|
+
),
|
|
38
|
+
output_file: Optional[Path] = typer.Option(
|
|
39
|
+
None, "--output", help="Save results to file"
|
|
40
|
+
),
|
|
41
|
+
timeout: int = typer.Option(300, "--timeout", help="Query timeout in seconds"),
|
|
42
|
+
fallback_branches: Optional[str] = typer.Option(
|
|
43
|
+
None, "--fallback-branches", help="Comma-separated list of fallback branch IDs"
|
|
44
|
+
),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Execute a SQL query and display results."""
|
|
47
|
+
console = Console()
|
|
48
|
+
formatter = OutputFormatter()
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Parse fallback branches if provided
|
|
52
|
+
fallback_branch_ids = (
|
|
53
|
+
fallback_branches.split(",") if fallback_branches else None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Create service and execute query
|
|
57
|
+
service = SqlService(profile=profile)
|
|
58
|
+
|
|
59
|
+
with SpinnerProgressTracker().track_spinner("Executing SQL query..."):
|
|
60
|
+
result = service.execute_query(
|
|
61
|
+
query=query,
|
|
62
|
+
fallback_branch_ids=fallback_branch_ids,
|
|
63
|
+
timeout=timeout,
|
|
64
|
+
format="table" if output_format in ["table", "csv"] else "json",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Extract results
|
|
68
|
+
query_results = result.get("results", {})
|
|
69
|
+
|
|
70
|
+
# Display results
|
|
71
|
+
if output_file:
|
|
72
|
+
formatter.save_to_file(query_results, output_file, output_format)
|
|
73
|
+
console.print(f"[green]Results saved to {output_file}[/green]")
|
|
74
|
+
else:
|
|
75
|
+
formatter.display(query_results, output_format)
|
|
76
|
+
|
|
77
|
+
# Show query metadata
|
|
78
|
+
if "query_id" in result:
|
|
79
|
+
console.print(f"\n[dim]Query ID: {result['query_id']}[/dim]")
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
formatter.print_error(f"Failed to execute query: {e}")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("submit")
|
|
87
|
+
def submit_query(
|
|
88
|
+
query: str = typer.Argument(..., help="SQL query to submit"),
|
|
89
|
+
profile: Optional[str] = typer.Option(
|
|
90
|
+
None, "--profile", help="Auth profile to use"
|
|
91
|
+
),
|
|
92
|
+
fallback_branches: Optional[str] = typer.Option(
|
|
93
|
+
None, "--fallback-branches", help="Comma-separated list of fallback branch IDs"
|
|
94
|
+
),
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Submit a SQL query without waiting for completion."""
|
|
97
|
+
console = Console()
|
|
98
|
+
formatter = OutputFormatter()
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Parse fallback branches if provided
|
|
102
|
+
fallback_branch_ids = (
|
|
103
|
+
fallback_branches.split(",") if fallback_branches else None
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Create service and submit query
|
|
107
|
+
service = SqlService(profile=profile)
|
|
108
|
+
|
|
109
|
+
with SpinnerProgressTracker().track_spinner("Submitting SQL query..."):
|
|
110
|
+
result = service.submit_query(
|
|
111
|
+
query=query, fallback_branch_ids=fallback_branch_ids
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
console.print("[green]Query submitted successfully[/green]")
|
|
115
|
+
console.print(f"Query ID: [bold]{result.get('query_id', 'N/A')}[/bold]")
|
|
116
|
+
console.print(f"Status: [yellow]{result.get('status', 'unknown')}[/yellow]")
|
|
117
|
+
|
|
118
|
+
if result.get("status") == "succeeded":
|
|
119
|
+
console.print("[green]Query completed immediately[/green]")
|
|
120
|
+
elif result.get("status") == "running":
|
|
121
|
+
console.print(
|
|
122
|
+
"Use [bold]pltr sql status <query-id>[/bold] to check progress"
|
|
123
|
+
)
|
|
124
|
+
console.print(
|
|
125
|
+
"Use [bold]pltr sql results <query-id>[/bold] to get results when completed"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
formatter.print_error(f"Failed to submit query: {e}")
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command("status")
|
|
134
|
+
def get_query_status(
|
|
135
|
+
query_id: str = typer.Argument(..., help="Query ID to check"),
|
|
136
|
+
profile: Optional[str] = typer.Option(
|
|
137
|
+
None, "--profile", help="Auth profile to use"
|
|
138
|
+
),
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Get the status of a submitted query."""
|
|
141
|
+
console = Console()
|
|
142
|
+
formatter = OutputFormatter()
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
service = SqlService(profile=profile)
|
|
146
|
+
|
|
147
|
+
with SpinnerProgressTracker().track_spinner("Checking query status..."):
|
|
148
|
+
result = service.get_query_status(query_id)
|
|
149
|
+
|
|
150
|
+
console.print(f"Query ID: [bold]{query_id}[/bold]")
|
|
151
|
+
|
|
152
|
+
status = result.get("status", "unknown")
|
|
153
|
+
if status == "running":
|
|
154
|
+
console.print(f"Status: [yellow]{status}[/yellow]")
|
|
155
|
+
console.print("Query is still executing...")
|
|
156
|
+
elif status == "succeeded":
|
|
157
|
+
console.print(f"Status: [green]{status}[/green]")
|
|
158
|
+
console.print("Use [bold]pltr sql results <query-id>[/bold] to get results")
|
|
159
|
+
elif status == "failed":
|
|
160
|
+
console.print(f"Status: [red]{status}[/red]")
|
|
161
|
+
error_msg = result.get("error_message", "Unknown error")
|
|
162
|
+
console.print(f"Error: {error_msg}")
|
|
163
|
+
elif status == "canceled":
|
|
164
|
+
console.print(f"Status: [red]{status}[/red]")
|
|
165
|
+
console.print("Query was canceled")
|
|
166
|
+
else:
|
|
167
|
+
console.print(f"Status: [dim]{status}[/dim]")
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
formatter.print_error(f"Failed to get query status: {e}")
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@app.command("results")
|
|
175
|
+
def get_query_results(
|
|
176
|
+
query_id: str = typer.Argument(..., help="Query ID to get results for"),
|
|
177
|
+
profile: Optional[str] = typer.Option(
|
|
178
|
+
None, "--profile", help="Auth profile to use"
|
|
179
|
+
),
|
|
180
|
+
output_format: str = typer.Option(
|
|
181
|
+
"table", "--format", help="Output format (table, json, csv)"
|
|
182
|
+
),
|
|
183
|
+
output_file: Optional[Path] = typer.Option(
|
|
184
|
+
None, "--output", help="Save results to file"
|
|
185
|
+
),
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Get the results of a completed query."""
|
|
188
|
+
console = Console()
|
|
189
|
+
formatter = OutputFormatter()
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
service = SqlService(profile=profile)
|
|
193
|
+
|
|
194
|
+
with SpinnerProgressTracker().track_spinner("Retrieving query results..."):
|
|
195
|
+
result = service.get_query_results(
|
|
196
|
+
query_id,
|
|
197
|
+
format="table" if output_format in ["table", "csv"] else "json",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Display or save results
|
|
201
|
+
if output_file:
|
|
202
|
+
formatter.save_to_file(result, output_file, output_format)
|
|
203
|
+
console.print(f"[green]Results saved to {output_file}[/green]")
|
|
204
|
+
else:
|
|
205
|
+
formatter.display(result, output_format)
|
|
206
|
+
|
|
207
|
+
console.print(f"\n[dim]Query ID: {query_id}[/dim]")
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
formatter.print_error(f"Failed to get query results: {e}")
|
|
211
|
+
raise typer.Exit(1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@app.command("cancel")
|
|
215
|
+
def cancel_query(
|
|
216
|
+
query_id: str = typer.Argument(..., help="Query ID to cancel"),
|
|
217
|
+
profile: Optional[str] = typer.Option(
|
|
218
|
+
None, "--profile", help="Auth profile to use"
|
|
219
|
+
),
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Cancel a running query."""
|
|
222
|
+
console = Console()
|
|
223
|
+
formatter = OutputFormatter()
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
service = SqlService(profile=profile)
|
|
227
|
+
|
|
228
|
+
with SpinnerProgressTracker().track_spinner("Canceling query..."):
|
|
229
|
+
result = service.cancel_query(query_id)
|
|
230
|
+
|
|
231
|
+
console.print(f"Query ID: [bold]{query_id}[/bold]")
|
|
232
|
+
|
|
233
|
+
status = result.get("status", "unknown")
|
|
234
|
+
if status == "canceled":
|
|
235
|
+
console.print(f"Status: [red]{status}[/red]")
|
|
236
|
+
console.print("Query has been canceled successfully")
|
|
237
|
+
else:
|
|
238
|
+
console.print(f"Status: [yellow]{status}[/yellow]")
|
|
239
|
+
console.print("Query may have already completed or was not running")
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
formatter.print_error(f"Failed to cancel query: {e}")
|
|
243
|
+
raise typer.Exit(1)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@app.command("export")
|
|
247
|
+
def export_query_results(
|
|
248
|
+
query: str = typer.Argument(..., help="SQL query to execute and export"),
|
|
249
|
+
output_file: Path = typer.Argument(..., help="Output file path"),
|
|
250
|
+
profile: Optional[str] = typer.Option(
|
|
251
|
+
None, "--profile", help="Auth profile to use"
|
|
252
|
+
),
|
|
253
|
+
output_format: Optional[str] = typer.Option(
|
|
254
|
+
None,
|
|
255
|
+
"--format",
|
|
256
|
+
help="Output format (auto-detected from file extension if not specified)",
|
|
257
|
+
),
|
|
258
|
+
timeout: int = typer.Option(300, "--timeout", help="Query timeout in seconds"),
|
|
259
|
+
fallback_branches: Optional[str] = typer.Option(
|
|
260
|
+
None, "--fallback-branches", help="Comma-separated list of fallback branch IDs"
|
|
261
|
+
),
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Execute a SQL query and export results to a file."""
|
|
264
|
+
console = Console()
|
|
265
|
+
formatter = OutputFormatter()
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
# Auto-detect format from file extension if not specified
|
|
269
|
+
if output_format is None:
|
|
270
|
+
ext = output_file.suffix.lower()
|
|
271
|
+
if ext == ".json":
|
|
272
|
+
output_format = "json"
|
|
273
|
+
elif ext == ".csv":
|
|
274
|
+
output_format = "csv"
|
|
275
|
+
else:
|
|
276
|
+
output_format = "table" # Default
|
|
277
|
+
|
|
278
|
+
# Parse fallback branches if provided
|
|
279
|
+
fallback_branch_ids = (
|
|
280
|
+
fallback_branches.split(",") if fallback_branches else None
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Create service and execute query
|
|
284
|
+
service = SqlService(profile=profile)
|
|
285
|
+
|
|
286
|
+
with SpinnerProgressTracker().track_spinner("Executing SQL query..."):
|
|
287
|
+
result = service.execute_query(
|
|
288
|
+
query=query,
|
|
289
|
+
fallback_branch_ids=fallback_branch_ids,
|
|
290
|
+
timeout=timeout,
|
|
291
|
+
format="table" if output_format in ["table", "csv"] else "json",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Save results to file
|
|
295
|
+
query_results = result.get("results", {})
|
|
296
|
+
formatter.save_to_file(query_results, output_file, output_format)
|
|
297
|
+
|
|
298
|
+
console.print(
|
|
299
|
+
f"[green]Query executed and results saved to {output_file}[/green]"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Show query metadata
|
|
303
|
+
if "query_id" in result:
|
|
304
|
+
console.print(f"[dim]Query ID: {result['query_id']}[/dim]")
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
formatter.print_error(f"Failed to export query results: {e}")
|
|
308
|
+
raise typer.Exit(1)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@app.command("wait")
|
|
312
|
+
def wait_for_query(
|
|
313
|
+
query_id: str = typer.Argument(..., help="Query ID to wait for"),
|
|
314
|
+
profile: Optional[str] = typer.Option(
|
|
315
|
+
None, "--profile", help="Auth profile to use"
|
|
316
|
+
),
|
|
317
|
+
timeout: int = typer.Option(300, "--timeout", help="Maximum wait time in seconds"),
|
|
318
|
+
output_format: str = typer.Option(
|
|
319
|
+
"table", "--format", help="Output format for results (table, json, csv)"
|
|
320
|
+
),
|
|
321
|
+
output_file: Optional[Path] = typer.Option(
|
|
322
|
+
None, "--output", help="Save results to file when completed"
|
|
323
|
+
),
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Wait for a query to complete and optionally display results."""
|
|
326
|
+
console = Console()
|
|
327
|
+
formatter = OutputFormatter()
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
service = SqlService(profile=profile)
|
|
331
|
+
|
|
332
|
+
with SpinnerProgressTracker().track_spinner("Waiting for query to complete..."):
|
|
333
|
+
status_result = service.wait_for_completion(query_id, timeout)
|
|
334
|
+
|
|
335
|
+
console.print(f"Query ID: [bold]{query_id}[/bold]")
|
|
336
|
+
console.print(
|
|
337
|
+
f"Status: [green]{status_result.get('status', 'completed')}[/green]"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Get and display results if requested
|
|
341
|
+
if output_file or output_format != "table":
|
|
342
|
+
with SpinnerProgressTracker().track_spinner("Retrieving results..."):
|
|
343
|
+
result = service.get_query_results(
|
|
344
|
+
query_id,
|
|
345
|
+
format="table" if output_format in ["table", "csv"] else "json",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if output_file:
|
|
349
|
+
formatter.save_to_file(result, output_file, output_format)
|
|
350
|
+
console.print(f"[green]Results saved to {output_file}[/green]")
|
|
351
|
+
else:
|
|
352
|
+
formatter.display(result, output_format)
|
|
353
|
+
else:
|
|
354
|
+
console.print("Use [bold]pltr sql results <query-id>[/bold] to get results")
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
formatter.print_error(f"Failed while waiting for query: {e}")
|
|
358
|
+
raise typer.Exit(1)
|
pltr/commands/verify.py
CHANGED
|
@@ -97,6 +97,7 @@ def verify(
|
|
|
97
97
|
"client_id": client_id,
|
|
98
98
|
"client_secret": client_secret,
|
|
99
99
|
},
|
|
100
|
+
timeout=30,
|
|
100
101
|
)
|
|
101
102
|
if token_response.status_code == 200:
|
|
102
103
|
access_token = token_response.json().get("access_token")
|
|
@@ -108,7 +109,7 @@ def verify(
|
|
|
108
109
|
raise typer.Exit(1)
|
|
109
110
|
|
|
110
111
|
# Make the request to /multipass/api/me
|
|
111
|
-
response = requests.get(url, headers=headers)
|
|
112
|
+
response = requests.get(url, headers=headers, timeout=30)
|
|
112
113
|
|
|
113
114
|
if response.status_code == 200:
|
|
114
115
|
user_info = response.json()
|
pltr/config/aliases.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Alias configuration management for pltr-cli."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from pltr.config.settings import Settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AliasManager:
|
|
13
|
+
"""Manages command aliases for pltr-cli."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
"""Initialize the alias manager."""
|
|
17
|
+
settings = Settings()
|
|
18
|
+
self.config_dir = settings.config_dir
|
|
19
|
+
self.aliases_file = self.config_dir / "aliases.json"
|
|
20
|
+
self.aliases = self._load_aliases()
|
|
21
|
+
|
|
22
|
+
def _load_aliases(self) -> Dict[str, str]:
|
|
23
|
+
"""Load aliases from the configuration file.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Dictionary mapping alias names to commands
|
|
27
|
+
"""
|
|
28
|
+
if not self.aliases_file.exists():
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
with open(self.aliases_file, "r") as f:
|
|
33
|
+
return json.load(f)
|
|
34
|
+
except (json.JSONDecodeError, IOError):
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
def _save_aliases(self) -> None:
|
|
38
|
+
"""Save aliases to the configuration file."""
|
|
39
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
with open(self.aliases_file, "w") as f:
|
|
41
|
+
json.dump(self.aliases, f, indent=2, sort_keys=True)
|
|
42
|
+
|
|
43
|
+
def add_alias(self, name: str, command: str) -> bool:
|
|
44
|
+
"""Add a new alias.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: Alias name
|
|
48
|
+
command: Command to alias
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if alias was added, False if it already exists
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If the alias would create a circular reference
|
|
55
|
+
"""
|
|
56
|
+
if name in self.aliases:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
# Check for circular references
|
|
60
|
+
if self._would_create_cycle(name, command):
|
|
61
|
+
raise ValueError(f"Alias '{name}' would create a circular reference")
|
|
62
|
+
|
|
63
|
+
self.aliases[name] = command
|
|
64
|
+
self._save_aliases()
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
def remove_alias(self, name: str) -> bool:
|
|
68
|
+
"""Remove an alias.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
name: Alias name to remove
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if alias was removed, False if it didn't exist
|
|
75
|
+
"""
|
|
76
|
+
if name not in self.aliases:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
del self.aliases[name]
|
|
80
|
+
self._save_aliases()
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def edit_alias(self, name: str, command: str) -> bool:
|
|
84
|
+
"""Edit an existing alias.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: Alias name to edit
|
|
88
|
+
command: New command for the alias
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if alias was edited, False if it doesn't exist
|
|
92
|
+
"""
|
|
93
|
+
if name not in self.aliases:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
# Check for circular references
|
|
97
|
+
if self._would_create_cycle(name, command):
|
|
98
|
+
raise ValueError(f"Alias '{name}' would create a circular reference")
|
|
99
|
+
|
|
100
|
+
self.aliases[name] = command
|
|
101
|
+
self._save_aliases()
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def get_alias(self, name: str) -> Optional[str]:
|
|
105
|
+
"""Get the command for an alias.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
name: Alias name
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The aliased command, or None if alias doesn't exist
|
|
112
|
+
"""
|
|
113
|
+
return self.aliases.get(name)
|
|
114
|
+
|
|
115
|
+
def list_aliases(self) -> Dict[str, str]:
|
|
116
|
+
"""Get all aliases.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Dictionary of all aliases
|
|
120
|
+
"""
|
|
121
|
+
return self.aliases.copy()
|
|
122
|
+
|
|
123
|
+
def resolve_alias(self, command: str, max_depth: int = 10) -> str:
|
|
124
|
+
"""Resolve an alias to its final command.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
command: Command that might be an alias
|
|
128
|
+
max_depth: Maximum recursion depth for nested aliases
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The resolved command
|
|
132
|
+
"""
|
|
133
|
+
resolved = command
|
|
134
|
+
depth = 0
|
|
135
|
+
seen = set()
|
|
136
|
+
|
|
137
|
+
while resolved in self.aliases and depth < max_depth:
|
|
138
|
+
if resolved in seen:
|
|
139
|
+
# Circular reference detected
|
|
140
|
+
return command
|
|
141
|
+
|
|
142
|
+
seen.add(resolved)
|
|
143
|
+
resolved = self.aliases[resolved]
|
|
144
|
+
depth += 1
|
|
145
|
+
|
|
146
|
+
return resolved
|
|
147
|
+
|
|
148
|
+
def _would_create_cycle(self, name: str, command: str) -> bool:
|
|
149
|
+
"""Check if adding/editing an alias would create a cycle.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
name: Alias name
|
|
153
|
+
command: Command to check
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if this would create a cycle
|
|
157
|
+
"""
|
|
158
|
+
# Direct self-reference
|
|
159
|
+
if command == name:
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
# Follow the chain starting from command
|
|
163
|
+
# If we ever reach 'name', it would create a cycle
|
|
164
|
+
current = command
|
|
165
|
+
visited = set()
|
|
166
|
+
|
|
167
|
+
while current in self.aliases:
|
|
168
|
+
# Check for existing cycles
|
|
169
|
+
if current in visited:
|
|
170
|
+
break
|
|
171
|
+
visited.add(current)
|
|
172
|
+
|
|
173
|
+
# Get what this alias points to
|
|
174
|
+
current = self.aliases[current]
|
|
175
|
+
|
|
176
|
+
# If we reached the name we're trying to add, it's a cycle
|
|
177
|
+
if current == name:
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def display_aliases(self, name: Optional[str] = None) -> None:
|
|
183
|
+
"""Display aliases in a formatted table.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
name: Optional specific alias to display
|
|
187
|
+
"""
|
|
188
|
+
if name:
|
|
189
|
+
command = self.get_alias(name)
|
|
190
|
+
if command:
|
|
191
|
+
rprint(f"[green]{name}[/green] → {command}")
|
|
192
|
+
else:
|
|
193
|
+
rprint(f"[red]Alias '{name}' not found[/red]")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if not self.aliases:
|
|
197
|
+
rprint("[yellow]No aliases configured[/yellow]")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
table = Table(title="Command Aliases", show_header=True)
|
|
201
|
+
table.add_column("Alias", style="cyan")
|
|
202
|
+
table.add_column("Command", style="green")
|
|
203
|
+
|
|
204
|
+
for alias_name, command in sorted(self.aliases.items()):
|
|
205
|
+
table.add_row(alias_name, command)
|
|
206
|
+
|
|
207
|
+
rprint(table)
|
|
208
|
+
|
|
209
|
+
def get_completion_items(self) -> List[str]:
|
|
210
|
+
"""Get alias names for shell completion.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
List of alias names
|
|
214
|
+
"""
|
|
215
|
+
return list(self.aliases.keys())
|
|
216
|
+
|
|
217
|
+
def clear_all(self) -> int:
|
|
218
|
+
"""Clear all aliases.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Number of aliases cleared
|
|
222
|
+
"""
|
|
223
|
+
count = len(self.aliases)
|
|
224
|
+
self.aliases = {}
|
|
225
|
+
self._save_aliases()
|
|
226
|
+
return count
|
|
227
|
+
|
|
228
|
+
def import_aliases(self, data: Dict[str, str]) -> int:
|
|
229
|
+
"""Import aliases from a dictionary.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
data: Dictionary of aliases to import
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Number of aliases imported
|
|
236
|
+
"""
|
|
237
|
+
count = 0
|
|
238
|
+
for name, command in data.items():
|
|
239
|
+
if not self._would_create_cycle(name, command):
|
|
240
|
+
self.aliases[name] = command
|
|
241
|
+
count += 1
|
|
242
|
+
|
|
243
|
+
if count > 0:
|
|
244
|
+
self._save_aliases()
|
|
245
|
+
|
|
246
|
+
return count
|
|
247
|
+
|
|
248
|
+
def export_aliases(self) -> Dict[str, str]:
|
|
249
|
+
"""Export all aliases.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dictionary of all aliases
|
|
253
|
+
"""
|
|
254
|
+
return self.aliases.copy()
|