onehouse-cli 0.1.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.
@@ -0,0 +1,10 @@
1
+ """
2
+ Onehouse CLI - A command-line tool for executing SQL commands against the Onehouse API.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from .cli import main as cli_main
8
+ from .configure import main as configure_main
9
+
10
+ __all__ = ["cli_main", "configure_main"]
onehouse_cli/cli.py ADDED
@@ -0,0 +1,390 @@
1
+ """
2
+ Onehouse CLI Tool
3
+ Execute SQL commands against Onehouse API using stored credentials.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import sys
9
+ import argparse
10
+ import requests
11
+ import time
12
+ from pathlib import Path
13
+
14
+
15
+ def load_credentials():
16
+ """Load credentials from ~/.onehouse/credentials file."""
17
+ credentials_file = Path.home() / ".onehouse" / "credentials"
18
+ if not credentials_file.exists():
19
+ print("Error: No credentials found. Run 'onehouse-cli-configure' first to set up credentials.")
20
+ return None
21
+
22
+ try:
23
+ with open(credentials_file, 'r') as f:
24
+ return json.load(f)
25
+ except (json.JSONDecodeError, IOError) as e:
26
+ print(f"Error reading credentials file: {e}")
27
+ return None
28
+
29
+
30
+ def get_profile_credentials(credentials, profile_name):
31
+ """Get credentials for a specific profile."""
32
+ if profile_name not in credentials:
33
+ available_profiles = list(credentials.keys())
34
+ print(f"Error: Profile '{profile_name}' not found.")
35
+ if available_profiles:
36
+ print(f"Available profiles: {', '.join(available_profiles)}")
37
+ else:
38
+ print("No profiles found. Run 'onehouse-cli-configure' to create one.")
39
+ return None
40
+
41
+ return credentials[profile_name]
42
+
43
+
44
+ def check_request_status(profile_creds, request_id):
45
+ """Check the status of a request using the status endpoint."""
46
+ api_endpoint = profile_creds.get('api_endpoint', 'https://api.onehouse.ai')
47
+ url = f"{api_endpoint}/v1/status/{request_id}"
48
+
49
+ headers = {
50
+ 'x-onehouse-account-uid': profile_creds['account_uid'],
51
+ 'x-onehouse-project-uid': profile_creds['project_uid'],
52
+ 'x-onehouse-api-key': profile_creds['api_key'],
53
+ 'x-onehouse-api-secret': profile_creds['api_secret'],
54
+ 'x-onehouse-region': profile_creds['project_region'],
55
+ 'x-onehouse-uuid': profile_creds['user_id']
56
+ }
57
+
58
+ try:
59
+ response = requests.get(url, headers=headers)
60
+
61
+ if response.status_code == 200:
62
+ result = response.json()
63
+ return result
64
+ else:
65
+ print(f"Status check failed with status {response.status_code}")
66
+ try:
67
+ error_data = response.json()
68
+ print("Error response:", json.dumps(error_data, indent=2))
69
+ except json.JSONDecodeError:
70
+ print("Error response:", response.text)
71
+ return None
72
+
73
+ except requests.exceptions.RequestException as e:
74
+ print(f"Status check request failed: {e}")
75
+ return None
76
+
77
+
78
+ def check_status_async(profile_creds, request_id):
79
+ """Check the status of a request asynchronously without timeout."""
80
+ print(f"Checking status for request ID: {request_id}")
81
+
82
+ status_result = check_request_status(profile_creds, request_id)
83
+
84
+ if status_result is None:
85
+ print("Failed to check status.")
86
+ return False
87
+
88
+ # Try different possible status field names
89
+ status = (status_result.get('status') or
90
+ status_result.get('state') or
91
+ status_result.get('jobStatus') or
92
+ status_result.get('requestStatus') or
93
+ status_result.get('apiStatus') or '').lower()
94
+
95
+ print(f"Current Status: {status}")
96
+
97
+ if status in ['completed', 'success', 'finished', 'done', 'succeed', 'api_operation_status_success']:
98
+ print("✅ Request completed successfully!")
99
+
100
+ # Only show result if it's not empty
101
+ result_to_show = status_result.get('apiResponse', status_result)
102
+ if result_to_show and result_to_show != {}:
103
+ print("Result:")
104
+ print(json.dumps(result_to_show, indent=2))
105
+ return True
106
+ elif status in ['failed', 'error', 'failure', 'api_operation_status_failed', 'api_operation_status_error']:
107
+ print("❌ Request failed!")
108
+ print("Error details:")
109
+ print(json.dumps(status_result, indent=2))
110
+ return False
111
+ elif status in ['running', 'processing', 'in_progress', 'pending', 'submitted', 'queued', 'api_operation_status_pending']:
112
+ print("⏳ Request is still in progress...")
113
+ print("Full status details:")
114
+ print(json.dumps(status_result, indent=2))
115
+ return True
116
+ else:
117
+ print(f"⚠️ Request has status: {status}")
118
+ print("Full status details:")
119
+ print(json.dumps(status_result, indent=2))
120
+ return True
121
+
122
+
123
+ def wait_for_completion(profile_creds, request_id, timeout_minutes=10):
124
+ """Poll the status endpoint until completion or timeout."""
125
+ timeout_seconds = timeout_minutes * 60
126
+ start_time = time.time()
127
+ last_status = None
128
+ progress_shown = False
129
+
130
+ while time.time() - start_time < timeout_seconds:
131
+ status_result = check_request_status(profile_creds, request_id)
132
+
133
+ if status_result is None:
134
+ print("Failed to check status, retrying in 1 second...")
135
+ time.sleep(1)
136
+ continue
137
+
138
+ # Try different possible status field names
139
+ status = (status_result.get('status') or
140
+ status_result.get('state') or
141
+ status_result.get('jobStatus') or
142
+ status_result.get('requestStatus') or
143
+ status_result.get('apiStatus') or '').lower()
144
+
145
+ # Only print status if it changed or it's the first time
146
+ if status != last_status:
147
+ print(f"Status: {status}")
148
+ last_status = status
149
+
150
+ if status in ['completed', 'success', 'finished', 'done', 'succeed', 'api_operation_status_success']:
151
+ print("✅ Command completed successfully!")
152
+
153
+ # Only show result if it's not empty
154
+ result_to_show = status_result.get('apiResponse', status_result)
155
+ if result_to_show and result_to_show != {}:
156
+ print("Final Result:")
157
+ print(json.dumps(result_to_show, indent=2))
158
+ return True
159
+ elif status in ['failed', 'error', 'failure', 'api_operation_status_failed', 'api_operation_status_error']:
160
+ print("❌ Command failed!")
161
+ print("Error details:")
162
+ print(json.dumps(status_result, indent=2))
163
+ return False
164
+ elif status in ['running', 'processing', 'in_progress', 'pending', 'submitted', 'queued', 'api_operation_status_pending']:
165
+ if not progress_shown:
166
+ print("⏳ Request is in progress...")
167
+ progress_shown = True
168
+ time.sleep(1)
169
+ else:
170
+ if not progress_shown:
171
+ print(f"⏳ Request status is '{status}', monitoring progress...")
172
+ progress_shown = True
173
+ time.sleep(1)
174
+
175
+ print(f"⏰ Timeout reached after {timeout_minutes} minutes")
176
+ print(f"You can check the status later using: onehouse-cli --check-status {request_id} --profile {profile_creds.get('name', 'your-profile')}")
177
+ return False
178
+
179
+
180
+ def execute_command(profile_creds, command, timeout_minutes=10):
181
+ """Execute a command against the Onehouse API and wait for completion."""
182
+ api_endpoint = profile_creds.get('api_endpoint', 'https://api.onehouse.ai')
183
+ url = f"{api_endpoint}/v1/resource/"
184
+
185
+ headers = {
186
+ 'x-onehouse-account-uid': profile_creds['account_uid'],
187
+ 'x-onehouse-project-uid': profile_creds['project_uid'],
188
+ 'x-onehouse-api-key': profile_creds['api_key'],
189
+ 'x-onehouse-api-secret': profile_creds['api_secret'],
190
+ 'Content-Type': 'application/json',
191
+ 'x-onehouse-region': profile_creds['project_region'],
192
+ 'x-onehouse-uuid': profile_creds['user_id']
193
+ }
194
+
195
+ # Add required request_id header
196
+ headers['x-onehouse-link-uid'] = profile_creds['request_id']
197
+
198
+ payload = {
199
+ "statement": command
200
+ }
201
+
202
+ try:
203
+ print(f"Executing command: {command}")
204
+ print("Making API request...")
205
+
206
+ response = requests.post(url, headers=headers, json=payload)
207
+
208
+ print(f"Response Status: {response.status_code}")
209
+
210
+ if response.status_code == 200:
211
+ try:
212
+ result = response.json()
213
+ print("Initial Response:")
214
+ print(json.dumps(result, indent=2))
215
+
216
+ # Extract request ID from response if available
217
+ async_request_id = result.get('requestId') or result.get('request_id') or result.get('id')
218
+
219
+ if async_request_id:
220
+ print(f"Request ID: {async_request_id}")
221
+ return wait_for_completion(profile_creds, async_request_id, timeout_minutes)
222
+ else:
223
+ print("No request ID found in response - command may have completed immediately")
224
+ return True
225
+
226
+ except json.JSONDecodeError:
227
+ print("Response (raw text):")
228
+ print(response.text)
229
+ return True
230
+ else:
231
+ print("Error Response:")
232
+ print(f"Status Code: {response.status_code}")
233
+ print("Headers:")
234
+ for header, value in response.headers.items():
235
+ print(f" {header}: {value}")
236
+ print("Body:")
237
+ try:
238
+ error_data = response.json()
239
+ print(json.dumps(error_data, indent=2))
240
+ except json.JSONDecodeError:
241
+ print(response.text if response.text else "(empty)")
242
+ return False
243
+
244
+ except requests.exceptions.RequestException as e:
245
+ print(f"Request failed: {e}")
246
+ return False
247
+
248
+
249
+ def list_profiles(credentials):
250
+ """List all available profiles."""
251
+ if not credentials:
252
+ print("No profiles found.")
253
+ return
254
+
255
+ default_profile = credentials.get('_default', None)
256
+
257
+ print("Available profiles:")
258
+ for profile_name, profile_data in credentials.items():
259
+ # Skip the _default entry
260
+ if profile_name == '_default':
261
+ continue
262
+
263
+ account_uid = profile_data.get('account_uid', 'N/A')
264
+ project_uid = profile_data.get('project_uid', 'N/A')
265
+ region = profile_data.get('project_region', 'N/A')
266
+ environment = profile_data.get('environment', 'production')
267
+
268
+ default_indicator = " (default)" if profile_name == default_profile else ""
269
+ print(f" - {profile_name}{default_indicator}")
270
+ print(f" Account: {account_uid}")
271
+ print(f" Project: {project_uid}")
272
+ print(f" Region: {region}")
273
+ print(f" Environment: {environment}")
274
+ print()
275
+
276
+
277
+ def main():
278
+ """Main function to run the CLI tool."""
279
+ parser = argparse.ArgumentParser(
280
+ description='Execute SQL commands against Onehouse API',
281
+ epilog='Examples:\n'
282
+ ' Execute command: onehouse-cli --command "ALTER OCU SET LIMIT = \'3\'" --profile myprofile\n'
283
+ ' Check status: onehouse-cli --check-status REQUEST_ID --profile myprofile',
284
+ formatter_class=argparse.RawDescriptionHelpFormatter
285
+ )
286
+
287
+ parser.add_argument(
288
+ '--command', '-c',
289
+ type=str,
290
+ help='SQL command to execute (backticks around job IDs will be preserved automatically)'
291
+ )
292
+
293
+ parser.add_argument(
294
+ '--profile', '-p',
295
+ type=str,
296
+ help='Profile name to use for authentication (uses default if not specified)'
297
+ )
298
+
299
+ parser.add_argument(
300
+ '--list-profiles', '-l',
301
+ action='store_true',
302
+ help='List all available profiles'
303
+ )
304
+
305
+ parser.add_argument(
306
+ '--timeout', '-t',
307
+ type=int,
308
+ default=10,
309
+ help='Timeout in minutes for command completion (default: 10)'
310
+ )
311
+
312
+ parser.add_argument(
313
+ '--check-status',
314
+ type=str,
315
+ help='Check the status of an existing request by providing its request ID'
316
+ )
317
+
318
+ args = parser.parse_args()
319
+
320
+ # Load credentials
321
+ credentials = load_credentials()
322
+ if not credentials:
323
+ return 1
324
+
325
+ # List profiles if requested
326
+ if args.list_profiles:
327
+ list_profiles(credentials)
328
+ return 0
329
+
330
+ # Handle check-status if requested
331
+ if args.check_status:
332
+ # Handle profile selection for status check
333
+ profile_name = args.profile
334
+ if not profile_name:
335
+ # Check if there's a default profile set
336
+ if '_default' in credentials:
337
+ profile_name = credentials['_default']
338
+ print(f"Using default profile: {profile_name}")
339
+ else:
340
+ print("Error: --profile is required for status check")
341
+ print("\nUse --list-profiles to see available profiles")
342
+ print("Or run 'onehouse-cli-configure' to set up a default profile")
343
+ return 1
344
+
345
+ # Get profile credentials
346
+ profile_creds = get_profile_credentials(credentials, profile_name)
347
+ if not profile_creds:
348
+ return 1
349
+
350
+ # Store profile name for better error messages
351
+ profile_creds['name'] = profile_name
352
+
353
+ # Check status asynchronously
354
+ success = check_status_async(profile_creds, args.check_status)
355
+ return 0 if success else 1
356
+
357
+ # Validate required arguments for command execution
358
+ if not args.command:
359
+ print("Error: --command is required (unless using --check-status)")
360
+ parser.print_help()
361
+ return 1
362
+
363
+ # Handle profile selection
364
+ profile_name = args.profile
365
+ if not profile_name:
366
+ # Check if there's a default profile set
367
+ if '_default' in credentials:
368
+ profile_name = credentials['_default']
369
+ print(f"Using default profile: {profile_name}")
370
+ else:
371
+ print("Error: --profile is required")
372
+ print("\nUse --list-profiles to see available profiles")
373
+ print("Or run 'onehouse-cli-configure' to set up a default profile")
374
+ return 1
375
+
376
+ # Get profile credentials
377
+ profile_creds = get_profile_credentials(credentials, profile_name)
378
+ if not profile_creds:
379
+ return 1
380
+
381
+ # Store profile name for better error messages
382
+ profile_creds['name'] = profile_name
383
+
384
+ # Execute the command
385
+ success = execute_command(profile_creds, args.command, args.timeout)
386
+ return 0 if success else 1
387
+
388
+
389
+ if __name__ == "__main__":
390
+ sys.exit(main())
@@ -0,0 +1,327 @@
1
+ """
2
+ Onehouse CLI Configuration Tool
3
+ Collects and stores Onehouse API credentials for future use.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import getpass
9
+ from pathlib import Path
10
+
11
+
12
+ def get_user_input(prompt, secure=False):
13
+ """Get input from user with optional secure input for secrets."""
14
+ if secure:
15
+ return getpass.getpass(prompt)
16
+ return input(prompt).strip()
17
+
18
+
19
+ def create_config_directory():
20
+ """Create the ~/.onehouse directory if it doesn't exist."""
21
+ config_dir = Path.home() / ".onehouse"
22
+ config_dir.mkdir(exist_ok=True)
23
+ return config_dir
24
+
25
+
26
+ def load_existing_credentials():
27
+ """Load existing credentials file or return empty dict."""
28
+ credentials_file = Path.home() / ".onehouse" / "credentials"
29
+ if credentials_file.exists():
30
+ try:
31
+ with open(credentials_file, 'r') as f:
32
+ return json.load(f)
33
+ except (json.JSONDecodeError, IOError):
34
+ return {}
35
+ return {}
36
+
37
+
38
+ def save_credentials(credentials):
39
+ """Save credentials to ~/.onehouse/credentials file."""
40
+ config_dir = create_config_directory()
41
+ credentials_file = config_dir / "credentials"
42
+
43
+ with open(credentials_file, 'w') as f:
44
+ json.dump(credentials, f, indent=2)
45
+
46
+ # Set restrictive permissions (readable only by owner)
47
+ os.chmod(credentials_file, 0o600)
48
+
49
+
50
+ def collect_credentials():
51
+ """Collect all required credentials from user."""
52
+ print("Onehouse CLI Configuration")
53
+ print("=" * 30)
54
+ print("Please provide your Onehouse API credentials:")
55
+ print()
56
+
57
+ credentials = {}
58
+
59
+ # Profile name
60
+ profile_name = get_user_input("Profile name: ")
61
+ if not profile_name:
62
+ print("Error: Profile name is required")
63
+ return None
64
+
65
+ # Load existing credentials to check if profile exists
66
+ existing_credentials = load_existing_credentials()
67
+ existing_profile = existing_credentials.get(profile_name, {})
68
+
69
+ if profile_name in existing_credentials:
70
+ print(f"\n⚠️ Profile '{profile_name}' already exists:")
71
+ print(f" Account: {existing_profile.get('account_uid', 'N/A')}")
72
+ print(f" Project: {existing_profile.get('project_uid', 'N/A')}")
73
+ print(f" Region: {existing_profile.get('project_region', 'N/A')}")
74
+ print(f" Environment: {existing_profile.get('environment', 'production')}")
75
+
76
+ # Show API Key/Secret previews in the summary
77
+ existing_key = existing_profile.get('api_key', '')
78
+ existing_secret = existing_profile.get('api_secret', '')
79
+ key_preview = existing_key[:4] + '...' if len(existing_key) > 4 else existing_key
80
+ secret_preview = existing_secret[:4] + '...' if len(existing_secret) > 4 else existing_secret
81
+ if existing_key:
82
+ print(f" API Key: {key_preview}")
83
+ if existing_secret:
84
+ print(f" API Secret: {secret_preview}")
85
+
86
+ print("\nPress Enter to keep existing values or enter new values:")
87
+
88
+ # Account UID
89
+ existing_account = existing_profile.get('account_uid', '')
90
+ prompt = f"Account UID ({existing_account}): " if existing_account else "Account UID: "
91
+ account_uid = get_user_input(prompt).strip()
92
+ if not account_uid:
93
+ if existing_account:
94
+ account_uid = existing_account
95
+ else:
96
+ print("Error: Account UID is required")
97
+ return None
98
+
99
+ # Project UID
100
+ existing_project = existing_profile.get('project_uid', '')
101
+ prompt = f"Project UID ({existing_project}): " if existing_project else "Project UID: "
102
+ project_uid = get_user_input(prompt).strip()
103
+ if not project_uid:
104
+ if existing_project:
105
+ project_uid = existing_project
106
+ else:
107
+ print("Error: Project UID is required")
108
+ return None
109
+
110
+ # API Key
111
+ existing_key = existing_profile.get('api_key', '')
112
+ key_preview = existing_key[:4] + '...' if len(existing_key) > 4 else existing_key
113
+ prompt = f"API Key ({key_preview}) (input masked): " if existing_key else "API Key (input masked): "
114
+ api_key = get_user_input(prompt, secure=True)
115
+ if not api_key:
116
+ if existing_key:
117
+ api_key = existing_key
118
+ else:
119
+ print("Error: API Key is required")
120
+ return None
121
+
122
+ # API Secret
123
+ existing_secret = existing_profile.get('api_secret', '')
124
+ secret_preview = existing_secret[:4] + '...' if len(existing_secret) > 4 else existing_secret
125
+ prompt = f"API Secret ({secret_preview}) (input masked): " if existing_secret else "API Secret (input masked): "
126
+ api_secret = get_user_input(prompt, secure=True)
127
+ if not api_secret:
128
+ if existing_secret:
129
+ api_secret = existing_secret
130
+ else:
131
+ print("Error: API Secret is required")
132
+ return None
133
+
134
+ # Project Region
135
+ existing_region = existing_profile.get('project_region', '')
136
+ prompt = f"Project Region ({existing_region}): " if existing_region else "Project Region: "
137
+ project_region = get_user_input(prompt).strip()
138
+ if not project_region:
139
+ if existing_region:
140
+ project_region = existing_region
141
+ else:
142
+ print("Error: Project Region is required")
143
+ return None
144
+
145
+ # User ID
146
+ existing_user = existing_profile.get('user_id', '')
147
+ prompt = f"User ID ({existing_user}): " if existing_user else "User ID: "
148
+ user_id = get_user_input(prompt).strip()
149
+ if not user_id:
150
+ if existing_user:
151
+ user_id = existing_user
152
+ else:
153
+ print("Error: User ID is required")
154
+ return None
155
+
156
+ # Request ID
157
+ existing_request = existing_profile.get('request_id', '')
158
+ prompt = f"Request ID ({existing_request}): " if existing_request else "Request ID: "
159
+ request_id = get_user_input(prompt).strip()
160
+ if not request_id:
161
+ if existing_request:
162
+ request_id = existing_request
163
+ else:
164
+ print("Error: Request ID is required")
165
+ return None
166
+
167
+ # Environment selection
168
+ existing_env = existing_profile.get('environment', 'production')
169
+ print()
170
+ print("Environment Selection:")
171
+ print("1. Production (api.onehouse.ai)")
172
+ print("2. Staging (staging-api.onehouse.ai)")
173
+ prompt = f"Choose environment (1 or 2) ({existing_env}): " if existing_env else "Choose environment (1 or 2): "
174
+ env_choice = get_user_input(prompt).strip()
175
+
176
+ if not env_choice and existing_env:
177
+ environment = existing_env
178
+ api_endpoint = existing_profile.get('api_endpoint',
179
+ 'https://staging-api.onehouse.ai' if existing_env == 'staging'
180
+ else 'https://api.onehouse.ai')
181
+ elif env_choice == "2":
182
+ environment = "staging"
183
+ api_endpoint = "https://staging-api.onehouse.ai"
184
+ else:
185
+ environment = "production"
186
+ api_endpoint = "https://api.onehouse.ai"
187
+
188
+ print(f"Selected: {environment} environment")
189
+
190
+ credentials[profile_name] = {
191
+ "account_uid": account_uid,
192
+ "project_uid": project_uid,
193
+ "api_key": api_key,
194
+ "api_secret": api_secret,
195
+ "project_region": project_region,
196
+ "user_id": user_id,
197
+ "request_id": request_id,
198
+ "environment": environment,
199
+ "api_endpoint": api_endpoint
200
+ }
201
+
202
+ # Ask if this should be the default profile
203
+ print()
204
+ set_as_default = get_user_input("Set this as the default profile? (y/n): ").strip().lower()
205
+ if set_as_default in ['y', 'yes']:
206
+ credentials['_default'] = profile_name
207
+
208
+ return credentials, profile_name
209
+
210
+
211
+ def is_already_in_shell_profile():
212
+ """Check if onehouse-cli is already added to shell profile."""
213
+ cli_dir = Path(__file__).parent.absolute()
214
+
215
+ # Detect shell and profile file
216
+ shell_profiles = [
217
+ Path.home() / ".zshrc",
218
+ Path.home() / ".bashrc",
219
+ Path.home() / ".bash_profile"
220
+ ]
221
+
222
+ for profile_file in shell_profiles:
223
+ if profile_file.exists():
224
+ try:
225
+ with open(profile_file, 'r') as f:
226
+ content = f.read()
227
+ if str(cli_dir) in content and "onehouse-cli" in content:
228
+ return True
229
+ except IOError:
230
+ pass
231
+
232
+ return False
233
+
234
+
235
+ def add_to_shell_profile():
236
+ """Add onehouse-cli tools to the user's shell profile."""
237
+ import subprocess
238
+
239
+ # Get the directory where the CLI tools are located
240
+ cli_dir = Path(__file__).parent.absolute()
241
+ path_export = f'export PATH="{cli_dir}:$PATH"'
242
+
243
+ # Detect shell and profile file
244
+ shell_profiles = [
245
+ Path.home() / ".zshrc",
246
+ Path.home() / ".bashrc",
247
+ Path.home() / ".bash_profile"
248
+ ]
249
+
250
+ profile_file = None
251
+ for profile in shell_profiles:
252
+ if profile.exists():
253
+ profile_file = profile
254
+ break
255
+
256
+ if not profile_file:
257
+ print("Could not find shell profile file (.zshrc, .bashrc, .bash_profile)")
258
+ print(f"Manually add this to your shell profile: {path_export}")
259
+ return
260
+
261
+ # Check if already added
262
+ try:
263
+ with open(profile_file, 'r') as f:
264
+ content = f.read()
265
+ if str(cli_dir) in content and "onehouse-cli" in content:
266
+ print(f"✓ Already added to {profile_file}")
267
+ return
268
+ except IOError:
269
+ pass
270
+
271
+ # Add to profile
272
+ try:
273
+ with open(profile_file, 'a') as f:
274
+ f.write(f"\n# Add onehouse-cli tools to PATH\n{path_export}\n")
275
+ print(f"✓ Added to {profile_file}")
276
+ print("Please run 'source ~/.zshrc' (or your shell profile) to apply changes")
277
+ except IOError as e:
278
+ print(f"Failed to write to {profile_file}: {e}")
279
+ print(f"Manually add this to your shell profile: {path_export}")
280
+
281
+
282
+ def main():
283
+ """Main function to run the configuration tool."""
284
+ try:
285
+ # Load existing credentials
286
+ existing_credentials = load_existing_credentials()
287
+
288
+ # Collect new credentials
289
+ result = collect_credentials()
290
+ if not result:
291
+ return 1
292
+
293
+ new_credentials, profile_name = result
294
+
295
+ # Merge with existing credentials
296
+ existing_credentials.update(new_credentials)
297
+
298
+ # Save credentials
299
+ save_credentials(existing_credentials)
300
+
301
+ print()
302
+ print(f"✓ Profile '{profile_name}' configured successfully!")
303
+ print(f"Credentials saved to: {Path.home() / '.onehouse' / 'credentials'}")
304
+ print()
305
+
306
+ # Check if already in shell profile, only ask if not already added
307
+ if is_already_in_shell_profile():
308
+ print("✓ onehouse-cli tools already in shell PATH")
309
+ else:
310
+ add_to_profile = input("Add onehouse-cli tools to your shell PATH? (y/n): ").strip().lower()
311
+ if add_to_profile in ['y', 'yes']:
312
+ add_to_shell_profile()
313
+
314
+ print("You can now use this profile with future Onehouse CLI commands.")
315
+
316
+ return 0
317
+
318
+ except KeyboardInterrupt:
319
+ print("\n\nConfiguration cancelled.")
320
+ return 1
321
+ except Exception as e:
322
+ print(f"\nError: {e}")
323
+ return 1
324
+
325
+
326
+ if __name__ == "__main__":
327
+ exit(main())
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: onehouse-cli
3
+ Version: 0.1.0
4
+ Summary: A command-line tool for executing SQL commands against the Onehouse API
5
+ Author-email: Sagar Lakshmipathy <18vidhyasagar@gmail.com>
6
+ License: Other/Proprietary
7
+ Keywords: onehouse,cli,sql
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Python: >=3.7
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: requests
20
+
21
+ # Onehouse CLI
22
+
23
+ A command-line tool for executing SQL commands against the Onehouse API with synchronous responses.
24
+
25
+ ## Setup
26
+ 1. **Create a virtual environment and install dependencies:**
27
+ ```bash
28
+ python3 -m venv venv
29
+ source venv/bin/activate
30
+ pip install -r requirements.txt
31
+ ```
32
+ 2. **Configure credentials:**
33
+ From withing the `onehouse-cli` directory, run:
34
+ ```bash
35
+ ./onehouse-cli-configure
36
+ ```
37
+ Prompts for: Profile name, Account UID, Project UID, API Key, API Secret, Project Region, User ID, Request ID, Environment (production or staging)
38
+
39
+ **Note:** The tool will ask if you want to automatically add the CLI tools to your shell PATH for global access.
40
+
41
+ 3. **Credentials are stored in:** `~/.onehouse/credentials`
42
+
43
+ ## Usage
44
+
45
+ **Execute SQL commands:**
46
+ ```bash
47
+ onehouse-cli --command "SHOW LAKES" --profile myprofile
48
+ ```
49
+
50
+ **List available profiles:**
51
+ ```bash
52
+ onehouse-cli --list-profiles
53
+ ```
54
+
55
+ **Custom timeout (default: 10 minutes):**
56
+ ```bash
57
+ onehouse-cli --command "ALTER OCU SET LIMIT = '3'" --profile myprofile --timeout 5
58
+ ```
59
+
60
+ ## Features
61
+
62
+ - **Synchronous execution**: Automatically polls for completion (1s intervals)
63
+ - **Multiple profiles**: Store and switch between different API credentials
64
+ - **Error handling**: Clear error messages with headers and response details
65
+ - **Timeout control**: Configurable wait time for long-running operations
66
+ - **Status checking**: Check the status of an existing request by providing its request ID
67
+
68
+ ## Examples
69
+
70
+ ```bash
71
+ # Configure first profile
72
+ ./onehouse-cli-configure
73
+
74
+ # Execute queries
75
+ onehouse-cli --command "SHOW LAKES" --profile apex
76
+ onehouse-cli --command "DELETE CLUSTER managed_cluster_retail" --profile apex
77
+
78
+ # Manage profiles
79
+ onehouse-cli --list-profiles
80
+
81
+ # Check status of an existing request
82
+ onehouse-cli --check-status "<REQUEST_ID>" --profile myprofile
83
+ ```
@@ -0,0 +1,8 @@
1
+ onehouse_cli/__init__.py,sha256=0rXH4kKswcoeAxoVQqjPWjasPEoW4IOgWyM3YxjZjsM,242
2
+ onehouse_cli/cli.py,sha256=rRDuBbqlOjrY39Jnf49jSNi1I4weKtAGioNjF5T4-rg,14134
3
+ onehouse_cli/configure.py,sha256=68yyYCvwgAqXW8WZOrT_1UkpmXfu2JGEjNgxtXFYjHg,11172
4
+ onehouse_cli-0.1.0.dist-info/METADATA,sha256=A-HlKcZq2thu1e8KS0XEYQRmaQ9QNDhQWd55ksRcId4,2628
5
+ onehouse_cli-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ onehouse_cli-0.1.0.dist-info/entry_points.txt,sha256=4htvgeLz-pkJyhtN2eqMyL3LyD9rJBrqhIjf6cqrMVE,108
7
+ onehouse_cli-0.1.0.dist-info/top_level.txt,sha256=1C7mOY8nVumj1CWQP_MjotLh8RRPEGfd1JH8cFOf8k8,13
8
+ onehouse_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ onehouse-cli = onehouse_cli.cli:main
3
+ onehouse-cli-configure = onehouse_cli.configure:main
@@ -0,0 +1 @@
1
+ onehouse_cli