pltr-cli 0.1.1__py3-none-any.whl → 0.2.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/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/services/__init__.py CHANGED
@@ -1 +1,5 @@
1
1
  """Service wrappers for Foundry SDK."""
2
+
3
+ from .admin import AdminService
4
+
5
+ __all__ = ["AdminService"]
pltr/services/admin.py ADDED
@@ -0,0 +1,314 @@
1
+ """
2
+ Admin service wrapper for Foundry SDK admin operations.
3
+ Provides a high-level interface for user, group, role, and organization management.
4
+ """
5
+
6
+ from typing import Any, Dict, Optional
7
+ import json
8
+
9
+ from .base import BaseService
10
+
11
+
12
+ class AdminService(BaseService):
13
+ """Service wrapper for Foundry admin operations."""
14
+
15
+ def _get_service(self) -> Any:
16
+ """Get the Foundry admin service."""
17
+ return self.client.admin
18
+
19
+ # User Management Methods
20
+ def list_users(
21
+ self, page_size: Optional[int] = None, page_token: Optional[str] = None
22
+ ) -> Dict[str, Any]:
23
+ """
24
+ List all users in the organization.
25
+
26
+ Args:
27
+ page_size: Maximum number of users to return per page
28
+ page_token: Token for pagination (from previous response)
29
+
30
+ Returns:
31
+ Dictionary containing user list and pagination info
32
+ """
33
+ try:
34
+ response = self.service.User.list(
35
+ page_size=page_size, page_token=page_token
36
+ )
37
+ return self._serialize_response(response)
38
+ except Exception as e:
39
+ raise RuntimeError(f"Failed to list users: {str(e)}")
40
+
41
+ def get_user(self, user_id: str) -> Dict[str, Any]:
42
+ """
43
+ Get a specific user by ID.
44
+
45
+ Args:
46
+ user_id: The user ID or RID
47
+
48
+ Returns:
49
+ Dictionary containing user information
50
+ """
51
+ try:
52
+ response = self.service.User.get(user_id)
53
+ return self._serialize_response(response)
54
+ except Exception as e:
55
+ raise RuntimeError(f"Failed to get user {user_id}: {str(e)}")
56
+
57
+ def get_current_user(self) -> Dict[str, Any]:
58
+ """
59
+ Get information about the current authenticated user.
60
+
61
+ Returns:
62
+ Dictionary containing current user information
63
+ """
64
+ try:
65
+ response = self.service.User.get_current()
66
+ return self._serialize_response(response)
67
+ except Exception as e:
68
+ raise RuntimeError(f"Failed to get current user: {str(e)}")
69
+
70
+ def search_users(
71
+ self,
72
+ query: str,
73
+ page_size: Optional[int] = None,
74
+ page_token: Optional[str] = None,
75
+ ) -> Dict[str, Any]:
76
+ """
77
+ Search for users by query string.
78
+
79
+ Args:
80
+ query: Search query string
81
+ page_size: Maximum number of users to return per page
82
+ page_token: Token for pagination (from previous response)
83
+
84
+ Returns:
85
+ Dictionary containing search results and pagination info
86
+ """
87
+ try:
88
+ response = self.service.User.search(
89
+ query=query, page_size=page_size, page_token=page_token
90
+ )
91
+ return self._serialize_response(response)
92
+ except Exception as e:
93
+ raise RuntimeError(f"Failed to search users: {str(e)}")
94
+
95
+ def get_user_markings(self, user_id: str) -> Dict[str, Any]:
96
+ """
97
+ Get markings/permissions for a specific user.
98
+
99
+ Args:
100
+ user_id: The user ID or RID
101
+
102
+ Returns:
103
+ Dictionary containing user markings information
104
+ """
105
+ try:
106
+ response = self.service.User.get_markings(user_id)
107
+ return self._serialize_response(response)
108
+ except Exception as e:
109
+ raise RuntimeError(f"Failed to get user markings for {user_id}: {str(e)}")
110
+
111
+ def revoke_user_tokens(self, user_id: str) -> Dict[str, Any]:
112
+ """
113
+ Revoke all tokens for a specific user.
114
+
115
+ Args:
116
+ user_id: The user ID or RID
117
+
118
+ Returns:
119
+ Dictionary containing operation result
120
+ """
121
+ try:
122
+ self.service.User.revoke_all_tokens(user_id)
123
+ return {
124
+ "success": True,
125
+ "message": f"All tokens revoked for user {user_id}",
126
+ }
127
+ except Exception as e:
128
+ raise RuntimeError(f"Failed to revoke tokens for user {user_id}: {str(e)}")
129
+
130
+ # Group Management Methods
131
+ def list_groups(
132
+ self, page_size: Optional[int] = None, page_token: Optional[str] = None
133
+ ) -> Dict[str, Any]:
134
+ """
135
+ List all groups in the organization.
136
+
137
+ Args:
138
+ page_size: Maximum number of groups to return per page
139
+ page_token: Token for pagination (from previous response)
140
+
141
+ Returns:
142
+ Dictionary containing group list and pagination info
143
+ """
144
+ try:
145
+ response = self.service.Group.list(
146
+ page_size=page_size, page_token=page_token
147
+ )
148
+ return self._serialize_response(response)
149
+ except Exception as e:
150
+ raise RuntimeError(f"Failed to list groups: {str(e)}")
151
+
152
+ def get_group(self, group_id: str) -> Dict[str, Any]:
153
+ """
154
+ Get a specific group by ID.
155
+
156
+ Args:
157
+ group_id: The group ID or RID
158
+
159
+ Returns:
160
+ Dictionary containing group information
161
+ """
162
+ try:
163
+ response = self.service.Group.get(group_id)
164
+ return self._serialize_response(response)
165
+ except Exception as e:
166
+ raise RuntimeError(f"Failed to get group {group_id}: {str(e)}")
167
+
168
+ def search_groups(
169
+ self,
170
+ query: str,
171
+ page_size: Optional[int] = None,
172
+ page_token: Optional[str] = None,
173
+ ) -> Dict[str, Any]:
174
+ """
175
+ Search for groups by query string.
176
+
177
+ Args:
178
+ query: Search query string
179
+ page_size: Maximum number of groups to return per page
180
+ page_token: Token for pagination (from previous response)
181
+
182
+ Returns:
183
+ Dictionary containing search results and pagination info
184
+ """
185
+ try:
186
+ response = self.service.Group.search(
187
+ query=query, page_size=page_size, page_token=page_token
188
+ )
189
+ return self._serialize_response(response)
190
+ except Exception as e:
191
+ raise RuntimeError(f"Failed to search groups: {str(e)}")
192
+
193
+ def create_group(
194
+ self,
195
+ name: str,
196
+ description: Optional[str] = None,
197
+ organization_rid: Optional[str] = None,
198
+ ) -> Dict[str, Any]:
199
+ """
200
+ Create a new group.
201
+
202
+ Args:
203
+ name: The group name
204
+ description: Optional group description
205
+ organization_rid: Optional organization RID
206
+
207
+ Returns:
208
+ Dictionary containing created group information
209
+ """
210
+ try:
211
+ # Build create request parameters
212
+ create_params = {"name": name}
213
+ if description:
214
+ create_params["description"] = description
215
+ if organization_rid:
216
+ create_params["organization_rid"] = organization_rid
217
+
218
+ response = self.service.Group.create(**create_params)
219
+ return self._serialize_response(response)
220
+ except Exception as e:
221
+ raise RuntimeError(f"Failed to create group '{name}': {str(e)}")
222
+
223
+ def delete_group(self, group_id: str) -> Dict[str, Any]:
224
+ """
225
+ Delete a specific group.
226
+
227
+ Args:
228
+ group_id: The group ID or RID
229
+
230
+ Returns:
231
+ Dictionary containing operation result
232
+ """
233
+ try:
234
+ self.service.Group.delete(group_id)
235
+ return {
236
+ "success": True,
237
+ "message": f"Group {group_id} deleted successfully",
238
+ }
239
+ except Exception as e:
240
+ raise RuntimeError(f"Failed to delete group {group_id}: {str(e)}")
241
+
242
+ # Organization Management Methods
243
+ def get_organization(self, organization_id: str) -> Dict[str, Any]:
244
+ """
245
+ Get organization information.
246
+
247
+ Args:
248
+ organization_id: The organization ID or RID
249
+
250
+ Returns:
251
+ Dictionary containing organization information
252
+ """
253
+ try:
254
+ response = self.service.Organization.get(organization_id)
255
+ return self._serialize_response(response)
256
+ except Exception as e:
257
+ raise RuntimeError(
258
+ f"Failed to get organization {organization_id}: {str(e)}"
259
+ )
260
+
261
+ # Role Management Methods
262
+ def get_role(self, role_id: str) -> Dict[str, Any]:
263
+ """
264
+ Get role information.
265
+
266
+ Args:
267
+ role_id: The role ID or RID
268
+
269
+ Returns:
270
+ Dictionary containing role information
271
+ """
272
+ try:
273
+ response = self.service.Role.get(role_id)
274
+ return self._serialize_response(response)
275
+ except Exception as e:
276
+ raise RuntimeError(f"Failed to get role {role_id}: {str(e)}")
277
+
278
+ def _serialize_response(self, response: Any) -> Dict[str, Any]:
279
+ """
280
+ Convert response object to serializable dictionary.
281
+
282
+ Args:
283
+ response: Response object from SDK
284
+
285
+ Returns:
286
+ Serializable dictionary representation
287
+ """
288
+ if response is None:
289
+ return {}
290
+
291
+ # Handle different response types
292
+ if hasattr(response, "dict"):
293
+ # Pydantic models
294
+ return response.dict()
295
+ elif hasattr(response, "__dict__"):
296
+ # Regular objects
297
+ result = {}
298
+ for key, value in response.__dict__.items():
299
+ if not key.startswith("_"):
300
+ try:
301
+ # Try to serialize the value
302
+ json.dumps(value)
303
+ result[key] = value
304
+ except (TypeError, ValueError):
305
+ # Convert non-serializable values to string
306
+ result[key] = str(value)
307
+ return result
308
+ else:
309
+ # Primitive types or already serializable
310
+ try:
311
+ json.dumps(response)
312
+ return response
313
+ except (TypeError, ValueError):
314
+ return {"data": str(response)}