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/services/sql.py ADDED
@@ -0,0 +1,340 @@
1
+ """
2
+ SQL service wrapper for Foundry SDK SQL queries.
3
+ Provides a high-level interface for executing SQL queries against Foundry datasets.
4
+ """
5
+
6
+ import time
7
+ from typing import Any, Dict, List, Optional, Union
8
+ import json
9
+
10
+ from foundry_sdk.v2.sql_queries.models import (
11
+ RunningQueryStatus,
12
+ SucceededQueryStatus,
13
+ FailedQueryStatus,
14
+ CanceledQueryStatus,
15
+ )
16
+
17
+ from .base import BaseService
18
+
19
+
20
+ class SqlService(BaseService):
21
+ """Service wrapper for Foundry SQL query operations."""
22
+
23
+ def _get_service(self) -> Any:
24
+ """Get the Foundry SQL queries service."""
25
+ return self.client.sql_queries.SqlQuery
26
+
27
+ def execute_query(
28
+ self,
29
+ query: str,
30
+ fallback_branch_ids: Optional[List[str]] = None,
31
+ timeout: int = 300,
32
+ format: str = "table",
33
+ ) -> Dict[str, Any]:
34
+ """
35
+ Execute a SQL query and wait for completion.
36
+
37
+ Args:
38
+ query: SQL query string
39
+ fallback_branch_ids: Optional list of branch IDs for fallback
40
+ timeout: Maximum time to wait for query completion (seconds)
41
+ format: Output format for results ('table', 'json', 'raw')
42
+
43
+ Returns:
44
+ Dictionary containing query results and metadata
45
+
46
+ Raises:
47
+ RuntimeError: If query execution fails or times out
48
+ """
49
+ try:
50
+ # Submit the query
51
+ status = self.service.execute(
52
+ query=query, fallback_branch_ids=fallback_branch_ids
53
+ )
54
+
55
+ # If the query completed immediately
56
+ if isinstance(status, SucceededQueryStatus):
57
+ return self._format_completed_query(status.query_id, format)
58
+ elif isinstance(status, FailedQueryStatus):
59
+ raise RuntimeError(f"Query failed: {status.error_message}")
60
+ elif isinstance(status, CanceledQueryStatus):
61
+ raise RuntimeError("Query was canceled")
62
+ elif isinstance(status, RunningQueryStatus):
63
+ # Wait for completion
64
+ return self._wait_for_query_completion(status.query_id, timeout, format)
65
+ else:
66
+ raise RuntimeError(f"Unknown query status type: {type(status)}")
67
+
68
+ except Exception as e:
69
+ if isinstance(e, RuntimeError):
70
+ raise
71
+ raise RuntimeError(f"Failed to execute query: {e}")
72
+
73
+ def submit_query(
74
+ self, query: str, fallback_branch_ids: Optional[List[str]] = None
75
+ ) -> Dict[str, Any]:
76
+ """
77
+ Submit a SQL query without waiting for completion.
78
+
79
+ Args:
80
+ query: SQL query string
81
+ fallback_branch_ids: Optional list of branch IDs for fallback
82
+
83
+ Returns:
84
+ Dictionary containing query ID and initial status
85
+
86
+ Raises:
87
+ RuntimeError: If query submission fails
88
+ """
89
+ try:
90
+ status = self.service.execute(
91
+ query=query, fallback_branch_ids=fallback_branch_ids
92
+ )
93
+ return self._format_query_status(status)
94
+ except Exception as e:
95
+ raise RuntimeError(f"Failed to submit query: {e}")
96
+
97
+ def get_query_status(self, query_id: str) -> Dict[str, Any]:
98
+ """
99
+ Get the status of a submitted query.
100
+
101
+ Args:
102
+ query_id: Query identifier
103
+
104
+ Returns:
105
+ Dictionary containing query status information
106
+
107
+ Raises:
108
+ RuntimeError: If status check fails
109
+ """
110
+ try:
111
+ status = self.service.get_status(query_id)
112
+ return self._format_query_status(status)
113
+ except Exception as e:
114
+ raise RuntimeError(f"Failed to get query status: {e}")
115
+
116
+ def get_query_results(self, query_id: str, format: str = "table") -> Dict[str, Any]:
117
+ """
118
+ Get the results of a completed query.
119
+
120
+ Args:
121
+ query_id: Query identifier
122
+ format: Output format ('table', 'json', 'raw')
123
+
124
+ Returns:
125
+ Dictionary containing query results
126
+
127
+ Raises:
128
+ RuntimeError: If results retrieval fails
129
+ """
130
+ try:
131
+ # First check if the query has completed successfully
132
+ status = self.service.get_status(query_id)
133
+ if not isinstance(status, SucceededQueryStatus):
134
+ status_info = self._format_query_status(status)
135
+ if isinstance(status, FailedQueryStatus):
136
+ raise RuntimeError(f"Query failed: {status.error_message}")
137
+ elif isinstance(status, CanceledQueryStatus):
138
+ raise RuntimeError("Query was canceled")
139
+ elif isinstance(status, RunningQueryStatus):
140
+ raise RuntimeError("Query is still running")
141
+ else:
142
+ raise RuntimeError(f"Query status: {status_info['status']}")
143
+
144
+ # Get the results
145
+ results_bytes = self.service.get_results(query_id)
146
+ return self._format_query_results(results_bytes, format)
147
+
148
+ except Exception as e:
149
+ if isinstance(e, RuntimeError):
150
+ raise
151
+ raise RuntimeError(f"Failed to get query results: {e}")
152
+
153
+ def cancel_query(self, query_id: str) -> Dict[str, Any]:
154
+ """
155
+ Cancel a running query.
156
+
157
+ Args:
158
+ query_id: Query identifier
159
+
160
+ Returns:
161
+ Dictionary containing cancellation status
162
+
163
+ Raises:
164
+ RuntimeError: If cancellation fails
165
+ """
166
+ try:
167
+ self.service.cancel(query_id)
168
+ # Get updated status after cancellation
169
+ status = self.service.get_status(query_id)
170
+ return self._format_query_status(status)
171
+ except Exception as e:
172
+ raise RuntimeError(f"Failed to cancel query: {e}")
173
+
174
+ def wait_for_completion(
175
+ self, query_id: str, timeout: int = 300, poll_interval: int = 2
176
+ ) -> Dict[str, Any]:
177
+ """
178
+ Wait for a query to complete.
179
+
180
+ Args:
181
+ query_id: Query identifier
182
+ timeout: Maximum time to wait (seconds)
183
+ poll_interval: Time between status checks (seconds)
184
+
185
+ Returns:
186
+ Dictionary containing final query status
187
+
188
+ Raises:
189
+ RuntimeError: If query fails or times out
190
+ """
191
+ start_time = time.time()
192
+
193
+ while time.time() - start_time < timeout:
194
+ try:
195
+ status = self.service.get_status(query_id)
196
+
197
+ if isinstance(status, SucceededQueryStatus):
198
+ return self._format_query_status(status)
199
+ elif isinstance(status, FailedQueryStatus):
200
+ raise RuntimeError(f"Query failed: {status.error_message}")
201
+ elif isinstance(status, CanceledQueryStatus):
202
+ raise RuntimeError("Query was canceled")
203
+ elif isinstance(status, RunningQueryStatus):
204
+ # Still running, continue waiting
205
+ time.sleep(poll_interval)
206
+ continue
207
+ else:
208
+ raise RuntimeError(f"Unknown status type: {type(status)}")
209
+
210
+ except Exception as e:
211
+ if isinstance(e, RuntimeError):
212
+ raise
213
+ raise RuntimeError(f"Error checking query status: {e}")
214
+
215
+ # Timeout reached
216
+ raise RuntimeError(f"Query timed out after {timeout} seconds")
217
+
218
+ def _wait_for_query_completion(
219
+ self, query_id: str, timeout: int, format: str
220
+ ) -> Dict[str, Any]:
221
+ """
222
+ Wait for query completion and return formatted results.
223
+
224
+ Args:
225
+ query_id: Query identifier
226
+ timeout: Maximum wait time
227
+ format: Result format
228
+
229
+ Returns:
230
+ Dictionary with query results
231
+ """
232
+ # Wait for completion
233
+ self.wait_for_completion(query_id, timeout)
234
+
235
+ # Get results
236
+ return self._format_completed_query(query_id, format)
237
+
238
+ def _format_completed_query(self, query_id: str, format: str) -> Dict[str, Any]:
239
+ """
240
+ Format a completed query's results.
241
+
242
+ Args:
243
+ query_id: Query identifier
244
+ format: Result format
245
+
246
+ Returns:
247
+ Formatted query results
248
+ """
249
+ results_bytes = self.service.get_results(query_id)
250
+ results = self._format_query_results(results_bytes, format)
251
+
252
+ return {
253
+ "query_id": query_id,
254
+ "status": "succeeded",
255
+ "results": results,
256
+ }
257
+
258
+ def _format_query_status(
259
+ self,
260
+ status: Union[
261
+ RunningQueryStatus,
262
+ SucceededQueryStatus,
263
+ FailedQueryStatus,
264
+ CanceledQueryStatus,
265
+ ],
266
+ ) -> Dict[str, Any]:
267
+ """
268
+ Format query status for consistent output.
269
+
270
+ Args:
271
+ status: Query status object
272
+
273
+ Returns:
274
+ Formatted status dictionary
275
+ """
276
+ base_info: Dict[str, Any] = {"status": status.type}
277
+
278
+ if isinstance(status, (RunningQueryStatus, SucceededQueryStatus)):
279
+ base_info["query_id"] = status.query_id
280
+ elif isinstance(status, FailedQueryStatus):
281
+ base_info["error_message"] = status.error_message
282
+
283
+ return base_info
284
+
285
+ def _format_query_results(self, results_bytes: bytes, format: str) -> Any:
286
+ """
287
+ Format query results based on the requested format.
288
+
289
+ Args:
290
+ results_bytes: Raw results from the API
291
+ format: Desired output format
292
+
293
+ Returns:
294
+ Formatted results
295
+ """
296
+ if format == "raw":
297
+ return results_bytes
298
+
299
+ # Try to decode as text first
300
+ try:
301
+ results_text = results_bytes.decode("utf-8")
302
+ except UnicodeDecodeError:
303
+ # If it's binary data, return as base64 or hex
304
+ return {
305
+ "type": "binary",
306
+ "size_bytes": len(results_bytes),
307
+ "data": results_bytes.hex()[:200] + "..."
308
+ if len(results_bytes) > 100
309
+ else results_bytes.hex(),
310
+ }
311
+
312
+ if format == "json":
313
+ try:
314
+ # Try to parse as JSON
315
+ return json.loads(results_text)
316
+ except json.JSONDecodeError:
317
+ # Return as text if not valid JSON
318
+ return {"text": results_text}
319
+
320
+ elif format == "table":
321
+ # For table format, we'll return structured data
322
+ # that the formatter can convert to a table
323
+ try:
324
+ # Try parsing as JSON first for structured data
325
+ data = json.loads(results_text)
326
+ if isinstance(data, list) and data and isinstance(data[0], dict):
327
+ # List of dictionaries - perfect for table format
328
+ return data
329
+ else:
330
+ return {"result": data}
331
+ except json.JSONDecodeError:
332
+ # Return as text data
333
+ lines = results_text.strip().split("\n")
334
+ if len(lines) == 1:
335
+ return {"result": lines[0]}
336
+ else:
337
+ return {"results": lines}
338
+
339
+ # Default: return as text
340
+ return {"text": results_text}
@@ -0,0 +1,56 @@
1
+ """Alias resolution utilities for CLI commands."""
2
+
3
+ import shlex
4
+ import sys
5
+ from typing import List, Optional
6
+
7
+ from pltr.config.aliases import AliasManager
8
+
9
+
10
+ def resolve_command_aliases(args: Optional[List[str]] = None) -> List[str]:
11
+ """Resolve command aliases in the argument list.
12
+
13
+ Args:
14
+ args: Command arguments (defaults to sys.argv[1:])
15
+
16
+ Returns:
17
+ Resolved command arguments
18
+ """
19
+ if args is None:
20
+ args = sys.argv[1:]
21
+
22
+ if not args:
23
+ return args
24
+
25
+ # Don't resolve aliases for certain commands
26
+ if args[0] in ["alias", "--help", "-h", "--version", "completion"]:
27
+ return args
28
+
29
+ # Check if the first argument is an alias
30
+ manager = AliasManager()
31
+ first_arg = args[0]
32
+
33
+ # Try to resolve as an alias
34
+ resolved = manager.resolve_alias(first_arg)
35
+
36
+ if resolved != first_arg:
37
+ # It's an alias - parse the resolved command
38
+ try:
39
+ resolved_parts = shlex.split(resolved)
40
+ # Replace the first argument with the resolved command parts
41
+ # and append any additional arguments
42
+ return resolved_parts + args[1:]
43
+ except ValueError:
44
+ # If parsing fails, return original args
45
+ return args
46
+
47
+ return args
48
+
49
+
50
+ def inject_alias_resolution() -> None:
51
+ """Inject alias resolution into sys.argv before CLI parsing."""
52
+ # Get resolved arguments
53
+ resolved_args = resolve_command_aliases()
54
+
55
+ # Replace sys.argv with resolved arguments
56
+ sys.argv = [sys.argv[0]] + resolved_args
@@ -0,0 +1,178 @@
1
+ """Shell completion utilities for pltr CLI."""
2
+
3
+ import os
4
+ from typing import List
5
+ from pathlib import Path
6
+ import json
7
+
8
+ from pltr.config.profiles import ProfileManager
9
+ from pltr.config.aliases import AliasManager
10
+
11
+
12
+ def get_cached_rids() -> List[str]:
13
+ """Get recently used RIDs from cache."""
14
+ cache_dir = Path.home() / ".cache" / "pltr"
15
+ rid_cache_file = cache_dir / "recent_rids.json"
16
+
17
+ if rid_cache_file.exists():
18
+ try:
19
+ with open(rid_cache_file) as f:
20
+ data = json.load(f)
21
+ return data.get("rids", [])
22
+ except Exception:
23
+ pass
24
+
25
+ # Return some example RIDs if no cache
26
+ return [
27
+ "ri.foundry.main.dataset.",
28
+ "ri.foundry.main.folder.",
29
+ "ri.foundry.main.ontology.",
30
+ ]
31
+
32
+
33
+ def cache_rid(rid: str):
34
+ """Cache a RID for future completions."""
35
+ cache_dir = Path.home() / ".cache" / "pltr"
36
+ cache_dir.mkdir(parents=True, exist_ok=True)
37
+ rid_cache_file = cache_dir / "recent_rids.json"
38
+
39
+ # Load existing cache
40
+ rids = []
41
+ if rid_cache_file.exists():
42
+ try:
43
+ with open(rid_cache_file) as f:
44
+ data = json.load(f)
45
+ rids = data.get("rids", [])
46
+ except Exception:
47
+ pass
48
+
49
+ # Add new RID (keep last 50)
50
+ if rid not in rids:
51
+ rids.insert(0, rid)
52
+ rids = rids[:50]
53
+
54
+ # Save cache
55
+ try:
56
+ with open(rid_cache_file, "w") as f:
57
+ json.dump({"rids": rids}, f)
58
+ except Exception:
59
+ pass
60
+
61
+
62
+ def complete_rid(incomplete: str):
63
+ """Complete RID arguments."""
64
+ rids = get_cached_rids()
65
+ return [rid for rid in rids if rid.startswith(incomplete)]
66
+
67
+
68
+ def complete_profile(incomplete: str):
69
+ """Complete profile names."""
70
+ try:
71
+ manager = ProfileManager()
72
+ profiles = manager.list_profiles()
73
+ return [profile for profile in profiles if profile.startswith(incomplete)]
74
+ except Exception:
75
+ return []
76
+
77
+
78
+ def complete_output_format(incomplete: str):
79
+ """Complete output format options."""
80
+ formats = ["table", "json", "csv"]
81
+ return [fmt for fmt in formats if fmt.startswith(incomplete)]
82
+
83
+
84
+ def complete_sql_query(incomplete: str):
85
+ """Complete SQL query templates."""
86
+ templates = [
87
+ "SELECT * FROM ",
88
+ "SELECT COUNT(*) FROM ",
89
+ "SELECT DISTINCT ",
90
+ "WHERE ",
91
+ "GROUP BY ",
92
+ "ORDER BY ",
93
+ "LIMIT 10",
94
+ "JOIN ",
95
+ "LEFT JOIN ",
96
+ "INNER JOIN ",
97
+ ]
98
+ return [tmpl for tmpl in templates if tmpl.lower().startswith(incomplete.lower())]
99
+
100
+
101
+ def complete_ontology_action(incomplete: str):
102
+ """Complete ontology action names."""
103
+ # This would ideally fetch from the API but for now return common patterns
104
+ actions = [
105
+ "create",
106
+ "update",
107
+ "delete",
108
+ "createOrUpdate",
109
+ "link",
110
+ "unlink",
111
+ ]
112
+ return [action for action in actions if action.startswith(incomplete)]
113
+
114
+
115
+ def complete_alias_names(incomplete: str):
116
+ """Complete alias names."""
117
+ manager = AliasManager()
118
+ aliases = manager.get_completion_items()
119
+ return [alias for alias in aliases if alias.startswith(incomplete)]
120
+
121
+
122
+ def complete_file_path(incomplete: str):
123
+ """Complete file paths."""
124
+ # This is handled by shell natively, but we can provide hints
125
+ path = Path(incomplete) if incomplete else Path.cwd()
126
+
127
+ if incomplete and not path.exists():
128
+ parent = path.parent
129
+ prefix = path.name
130
+ else:
131
+ parent = path if path.is_dir() else path.parent
132
+ prefix = ""
133
+
134
+ try:
135
+ items = []
136
+ for item in parent.iterdir():
137
+ if item.name.startswith(prefix):
138
+ # Return path strings - shell will handle directory indicators
139
+ items.append(str(item))
140
+ return items
141
+ except Exception:
142
+ return []
143
+
144
+
145
+ def setup_completion_environment():
146
+ """Set up environment for shell completion support."""
147
+ # This is called when the CLI starts to register completion handlers
148
+
149
+ # Check if we're in completion mode
150
+ if os.environ.get("_PLTR_COMPLETE"):
151
+ # We're generating completions
152
+ # Set up any necessary context
153
+ pass
154
+
155
+
156
+ def handle_completion():
157
+ """Handle shell completion requests."""
158
+ # This is the main entry point for completion handling
159
+ # It's called when _PLTR_COMPLETE environment variable is set
160
+
161
+ complete_var = os.environ.get("_PLTR_COMPLETE")
162
+ if not complete_var:
163
+ return False
164
+
165
+ # Click handles the completion automatically through Typer
166
+ # Our custom completion functions are registered via autocompletion parameter
167
+ return True
168
+
169
+
170
+ # Register custom completion functions for specific parameter types
171
+ COMPLETION_FUNCTIONS = {
172
+ "rid": complete_rid,
173
+ "profile": complete_profile,
174
+ "output_format": complete_output_format,
175
+ "sql_query": complete_sql_query,
176
+ "ontology_action": complete_ontology_action,
177
+ "file_path": complete_file_path,
178
+ }