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/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,170 @@
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
+
10
+
11
+ def get_cached_rids() -> List[str]:
12
+ """Get recently used RIDs from cache."""
13
+ cache_dir = Path.home() / ".cache" / "pltr"
14
+ rid_cache_file = cache_dir / "recent_rids.json"
15
+
16
+ if rid_cache_file.exists():
17
+ try:
18
+ with open(rid_cache_file) as f:
19
+ data = json.load(f)
20
+ return data.get("rids", [])
21
+ except Exception:
22
+ pass
23
+
24
+ # Return some example RIDs if no cache
25
+ return [
26
+ "ri.foundry.main.dataset.",
27
+ "ri.foundry.main.folder.",
28
+ "ri.foundry.main.ontology.",
29
+ ]
30
+
31
+
32
+ def cache_rid(rid: str):
33
+ """Cache a RID for future completions."""
34
+ cache_dir = Path.home() / ".cache" / "pltr"
35
+ cache_dir.mkdir(parents=True, exist_ok=True)
36
+ rid_cache_file = cache_dir / "recent_rids.json"
37
+
38
+ # Load existing cache
39
+ rids = []
40
+ if rid_cache_file.exists():
41
+ try:
42
+ with open(rid_cache_file) as f:
43
+ data = json.load(f)
44
+ rids = data.get("rids", [])
45
+ except Exception:
46
+ pass
47
+
48
+ # Add new RID (keep last 50)
49
+ if rid not in rids:
50
+ rids.insert(0, rid)
51
+ rids = rids[:50]
52
+
53
+ # Save cache
54
+ try:
55
+ with open(rid_cache_file, "w") as f:
56
+ json.dump({"rids": rids}, f)
57
+ except Exception:
58
+ pass
59
+
60
+
61
+ def complete_rid(incomplete: str):
62
+ """Complete RID arguments."""
63
+ rids = get_cached_rids()
64
+ return [rid for rid in rids if rid.startswith(incomplete)]
65
+
66
+
67
+ def complete_profile(incomplete: str):
68
+ """Complete profile names."""
69
+ try:
70
+ manager = ProfileManager()
71
+ profiles = manager.list_profiles()
72
+ return [profile for profile in profiles if profile.startswith(incomplete)]
73
+ except Exception:
74
+ return []
75
+
76
+
77
+ def complete_output_format(incomplete: str):
78
+ """Complete output format options."""
79
+ formats = ["table", "json", "csv"]
80
+ return [fmt for fmt in formats if fmt.startswith(incomplete)]
81
+
82
+
83
+ def complete_sql_query(incomplete: str):
84
+ """Complete SQL query templates."""
85
+ templates = [
86
+ "SELECT * FROM ",
87
+ "SELECT COUNT(*) FROM ",
88
+ "SELECT DISTINCT ",
89
+ "WHERE ",
90
+ "GROUP BY ",
91
+ "ORDER BY ",
92
+ "LIMIT 10",
93
+ "JOIN ",
94
+ "LEFT JOIN ",
95
+ "INNER JOIN ",
96
+ ]
97
+ return [tmpl for tmpl in templates if tmpl.lower().startswith(incomplete.lower())]
98
+
99
+
100
+ def complete_ontology_action(incomplete: str):
101
+ """Complete ontology action names."""
102
+ # This would ideally fetch from the API but for now return common patterns
103
+ actions = [
104
+ "create",
105
+ "update",
106
+ "delete",
107
+ "createOrUpdate",
108
+ "link",
109
+ "unlink",
110
+ ]
111
+ return [action for action in actions if action.startswith(incomplete)]
112
+
113
+
114
+ def complete_file_path(incomplete: str):
115
+ """Complete file paths."""
116
+ # This is handled by shell natively, but we can provide hints
117
+ path = Path(incomplete) if incomplete else Path.cwd()
118
+
119
+ if incomplete and not path.exists():
120
+ parent = path.parent
121
+ prefix = path.name
122
+ else:
123
+ parent = path if path.is_dir() else path.parent
124
+ prefix = ""
125
+
126
+ try:
127
+ items = []
128
+ for item in parent.iterdir():
129
+ if item.name.startswith(prefix):
130
+ # Return path strings - shell will handle directory indicators
131
+ items.append(str(item))
132
+ return items
133
+ except Exception:
134
+ return []
135
+
136
+
137
+ def setup_completion_environment():
138
+ """Set up environment for shell completion support."""
139
+ # This is called when the CLI starts to register completion handlers
140
+
141
+ # Check if we're in completion mode
142
+ if os.environ.get("_PLTR_COMPLETE"):
143
+ # We're generating completions
144
+ # Set up any necessary context
145
+ pass
146
+
147
+
148
+ def handle_completion():
149
+ """Handle shell completion requests."""
150
+ # This is the main entry point for completion handling
151
+ # It's called when _PLTR_COMPLETE environment variable is set
152
+
153
+ complete_var = os.environ.get("_PLTR_COMPLETE")
154
+ if not complete_var:
155
+ return False
156
+
157
+ # Click handles the completion automatically through Typer
158
+ # Our custom completion functions are registered via autocompletion parameter
159
+ return True
160
+
161
+
162
+ # Register custom completion functions for specific parameter types
163
+ COMPLETION_FUNCTIONS = {
164
+ "rid": complete_rid,
165
+ "profile": complete_profile,
166
+ "output_format": complete_output_format,
167
+ "sql_query": complete_sql_query,
168
+ "ontology_action": complete_ontology_action,
169
+ "file_path": complete_file_path,
170
+ }