mantatech-sdk 0.5b0.dev65__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.
- manta/__init__.light.py +22 -0
- manta/__init__.py +83 -0
- manta/__main__.py +21 -0
- manta/apis/__init__.py +7 -0
- manta/apis/async_user_api.py +6458 -0
- manta/apis/graph.py +498 -0
- manta/apis/module.py +316 -0
- manta/apis/results.py +251 -0
- manta/apis/swarm.py +206 -0
- manta/apis/user_api.py +1016 -0
- manta/cli/__init__.py +1 -0
- manta/cli/commands/__init__.py +1 -0
- manta/cli/commands/base_handler.py +229 -0
- manta/cli/commands/doc.py +192 -0
- manta/cli/commands/install.py +346 -0
- manta/cli/commands/sdk.py +9 -0
- manta/cli/commands/sdk_cluster.py +211 -0
- manta/cli/commands/sdk_config.py +347 -0
- manta/cli/commands/sdk_globals.py +280 -0
- manta/cli/commands/sdk_logs.py +174 -0
- manta/cli/commands/sdk_main.py +167 -0
- manta/cli/commands/sdk_module.py +516 -0
- manta/cli/commands/sdk_nodes.py +168 -0
- manta/cli/commands/sdk_original.py +3873 -0
- manta/cli/commands/sdk_results.py +265 -0
- manta/cli/commands/sdk_swarm.py +454 -0
- manta/cli/commands/sdk_user.py +234 -0
- manta/cli/commands/status.py +292 -0
- manta/cli/component_detector.py +112 -0
- manta/cli/config_manager.py +445 -0
- manta/cli/main.py +265 -0
- manta/cli/utils/__init__.py +27 -0
- manta/cli/utils/converters.py +140 -0
- manta/clients/cluster_management_client.py +486 -0
- manta/clients/local_client.py +149 -0
- manta/clients/module_management_client.py +217 -0
- manta/clients/swarm_management_client.py +562 -0
- manta/clients/user_management_client.py +395 -0
- manta/clients/world_client.py +195 -0
- manta/light/__init__.py +31 -0
- manta/light/globals.py +245 -0
- manta/light/local.py +407 -0
- manta/light/logging_config.py +39 -0
- manta/light/path.py +116 -0
- manta/light/results.py +236 -0
- manta/light/task.py +100 -0
- manta/light/utils.py +217 -0
- manta/light/world.py +177 -0
- mantatech_sdk-0.5b0.dev65.dist-info/METADATA +1039 -0
- mantatech_sdk-0.5b0.dev65.dist-info/RECORD +54 -0
- mantatech_sdk-0.5b0.dev65.dist-info/WHEEL +5 -0
- mantatech_sdk-0.5b0.dev65.dist-info/entry_points.txt +2 -0
- mantatech_sdk-0.5b0.dev65.dist-info/licenses/LICENSE +683 -0
- mantatech_sdk-0.5b0.dev65.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""SDK module management commands."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.prompt import Confirm, Prompt
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.tree import Tree
|
|
10
|
+
|
|
11
|
+
from .base_handler import BaseSDKHandler
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SDKModuleHandler(BaseSDKHandler):
|
|
15
|
+
"""Handle module-related SDK commands."""
|
|
16
|
+
|
|
17
|
+
def add_subparsers(self, parent_parser):
|
|
18
|
+
"""Add module command subparsers."""
|
|
19
|
+
self.parser = parent_parser # Store parser reference
|
|
20
|
+
subparsers = parent_parser.add_subparsers(
|
|
21
|
+
dest="module_command", help="Module commands"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# List modules command
|
|
25
|
+
list_parser = subparsers.add_parser("list", help="List all user modules")
|
|
26
|
+
|
|
27
|
+
# List module IDs command
|
|
28
|
+
list_ids_parser = subparsers.add_parser("list-ids", help="List module IDs only")
|
|
29
|
+
|
|
30
|
+
# Show module command
|
|
31
|
+
show_parser = subparsers.add_parser(
|
|
32
|
+
"show", help="Show detailed module information"
|
|
33
|
+
)
|
|
34
|
+
show_parser.add_argument("module_id", help="Module ID to show")
|
|
35
|
+
|
|
36
|
+
# Upload/Push module command (using 'push' for consistency)
|
|
37
|
+
push_parser = subparsers.add_parser("push", help="Upload a module from file")
|
|
38
|
+
push_parser.add_argument("module_file", help="Path to module file")
|
|
39
|
+
push_parser.add_argument("--module-id", help="Optional module ID")
|
|
40
|
+
|
|
41
|
+
# Update module command
|
|
42
|
+
update_parser = subparsers.add_parser(
|
|
43
|
+
"update", help="Update an existing module"
|
|
44
|
+
)
|
|
45
|
+
update_parser.add_argument("module_id", help="Module ID to update")
|
|
46
|
+
update_parser.add_argument("module_file", help="Path to new module file")
|
|
47
|
+
|
|
48
|
+
# Delete module command
|
|
49
|
+
delete_parser = subparsers.add_parser("delete", help="Delete a module")
|
|
50
|
+
delete_parser.add_argument("module_id", help="Module ID to delete")
|
|
51
|
+
delete_parser.add_argument(
|
|
52
|
+
"--force", action="store_true", help="Skip confirmation"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Download/Get module command (using 'get' for consistency)
|
|
56
|
+
get_parser = subparsers.add_parser("get", help="Download a module to file")
|
|
57
|
+
get_parser.add_argument("module_id", help="Module ID to download")
|
|
58
|
+
get_parser.add_argument("--output", help="Output file or directory path")
|
|
59
|
+
|
|
60
|
+
# Add profile override to all subcommands
|
|
61
|
+
for parser in [
|
|
62
|
+
list_parser,
|
|
63
|
+
list_ids_parser,
|
|
64
|
+
show_parser,
|
|
65
|
+
push_parser,
|
|
66
|
+
update_parser,
|
|
67
|
+
delete_parser,
|
|
68
|
+
get_parser,
|
|
69
|
+
]:
|
|
70
|
+
parser.add_argument("--profile", help="Use specific profile")
|
|
71
|
+
|
|
72
|
+
def handle(self, args) -> int:
|
|
73
|
+
"""Handle module commands."""
|
|
74
|
+
if not args.module_command:
|
|
75
|
+
if hasattr(self, "parser"):
|
|
76
|
+
self.parser.print_help()
|
|
77
|
+
return 0 # Return 0 for help display
|
|
78
|
+
|
|
79
|
+
if args.module_command == "list":
|
|
80
|
+
return self.list_modules()
|
|
81
|
+
elif args.module_command == "list-ids":
|
|
82
|
+
return self.list_module_ids()
|
|
83
|
+
elif args.module_command == "show":
|
|
84
|
+
return self.show_module(args.module_id)
|
|
85
|
+
elif args.module_command == "push":
|
|
86
|
+
return self.upload_module(
|
|
87
|
+
args.module_file, getattr(args, "module_id", None)
|
|
88
|
+
)
|
|
89
|
+
elif args.module_command == "update":
|
|
90
|
+
return self.update_module(args.module_id, args.module_file)
|
|
91
|
+
elif args.module_command == "delete":
|
|
92
|
+
return self.delete_module(args.module_id, getattr(args, "force", False))
|
|
93
|
+
elif args.module_command == "get":
|
|
94
|
+
return self.download_module(args.module_id, getattr(args, "output", None))
|
|
95
|
+
else:
|
|
96
|
+
self.print_error(f"Unknown module command: {args.module_command}")
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
def list_modules(self) -> int:
|
|
100
|
+
"""List all modules for the user."""
|
|
101
|
+
api = self._get_user_api()
|
|
102
|
+
if not api:
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
|
|
107
|
+
async def run():
|
|
108
|
+
modules = await api.stream_and_fetch_modules()
|
|
109
|
+
return modules
|
|
110
|
+
|
|
111
|
+
modules = self._run_with_progress(run, "Fetching modules...")
|
|
112
|
+
|
|
113
|
+
if modules is None:
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
if not modules:
|
|
117
|
+
self.print("No modules found.")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
# Display modules
|
|
121
|
+
table = Table(title="User Modules")
|
|
122
|
+
table.add_column("Module ID", style="cyan")
|
|
123
|
+
table.add_column("Name", style="blue")
|
|
124
|
+
table.add_column("Type", style="green")
|
|
125
|
+
table.add_column("Size", style="magenta")
|
|
126
|
+
table.add_column("Created", style="yellow")
|
|
127
|
+
|
|
128
|
+
for module in modules:
|
|
129
|
+
# UserAPI returns Module objects (not dicts, not protos)
|
|
130
|
+
module_id = (
|
|
131
|
+
module.module_id
|
|
132
|
+
if hasattr(module, "module_id") and module.module_id
|
|
133
|
+
else ""
|
|
134
|
+
)
|
|
135
|
+
name = (
|
|
136
|
+
module.name
|
|
137
|
+
if hasattr(module, "name") and module.name
|
|
138
|
+
else "Unknown"
|
|
139
|
+
)
|
|
140
|
+
image = (
|
|
141
|
+
module.image
|
|
142
|
+
if hasattr(module, "image") and module.image
|
|
143
|
+
else "Python"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Get size from python_files if available
|
|
147
|
+
data_size = 0
|
|
148
|
+
if hasattr(module, "python_files") and module.python_files:
|
|
149
|
+
# Sum the size of all files
|
|
150
|
+
data_size = sum(
|
|
151
|
+
len(content) for content in module.python_files.values()
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
table.add_row(
|
|
155
|
+
module_id,
|
|
156
|
+
name,
|
|
157
|
+
image,
|
|
158
|
+
self._format_size(data_size),
|
|
159
|
+
"N/A", # Created timestamp not available
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self.console.print(table)
|
|
163
|
+
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self.print_error(f"Failed to list modules: {e}")
|
|
168
|
+
return 1
|
|
169
|
+
|
|
170
|
+
def list_module_ids(self) -> int:
|
|
171
|
+
"""List module IDs only."""
|
|
172
|
+
api = self._get_user_api()
|
|
173
|
+
if not api:
|
|
174
|
+
return 1
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
|
|
178
|
+
async def run():
|
|
179
|
+
return await api.list_module_ids()
|
|
180
|
+
|
|
181
|
+
module_ids = self._run_with_progress(run, "Fetching module IDs...")
|
|
182
|
+
|
|
183
|
+
if module_ids is None:
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
if not module_ids:
|
|
187
|
+
self.print("No modules found.")
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
self.print(f"Found {len(module_ids)} modules:")
|
|
191
|
+
for module_id in module_ids:
|
|
192
|
+
self.print(f" {module_id}")
|
|
193
|
+
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
self.print_error(f"Failed to list module IDs: {e}")
|
|
198
|
+
return 1
|
|
199
|
+
|
|
200
|
+
def show_module(self, module_id: str) -> int:
|
|
201
|
+
"""Show detailed module information."""
|
|
202
|
+
api = self._get_user_api()
|
|
203
|
+
if not api:
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
|
|
208
|
+
async def run():
|
|
209
|
+
return await api.get_module(module_id)
|
|
210
|
+
|
|
211
|
+
module = self._run_with_progress(run, "Fetching module details...")
|
|
212
|
+
|
|
213
|
+
if module is None:
|
|
214
|
+
return 1
|
|
215
|
+
|
|
216
|
+
# Display module details
|
|
217
|
+
# UserAPI returns Module objects
|
|
218
|
+
name = module.name if hasattr(module, "name") and module.name else "Unknown"
|
|
219
|
+
image = (
|
|
220
|
+
module.image if hasattr(module, "image") and module.image else "Python"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Get size from python_files if available
|
|
224
|
+
data_size = 0
|
|
225
|
+
if hasattr(module, "python_files") and module.python_files:
|
|
226
|
+
data_size = sum(
|
|
227
|
+
len(content) for content in module.python_files.values()
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
panel = Panel.fit(
|
|
231
|
+
f"[cyan]Module ID:[/cyan] {module_id}\n"
|
|
232
|
+
f"[cyan]Name:[/cyan] {name}\n"
|
|
233
|
+
f"[cyan]Type:[/cyan] {image}\n"
|
|
234
|
+
f"[cyan]Size:[/cyan] {self._format_size(data_size)}\n"
|
|
235
|
+
f"[cyan]Created:[/cyan] N/A",
|
|
236
|
+
title=f"Module: {module_id}",
|
|
237
|
+
)
|
|
238
|
+
self.console.print(panel)
|
|
239
|
+
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
except Exception as e:
|
|
243
|
+
self.print_error(f"Failed to get module details: {e}")
|
|
244
|
+
return 1
|
|
245
|
+
|
|
246
|
+
def upload_module(self, module_file: str, module_id: Optional[str] = None) -> int:
|
|
247
|
+
"""Upload a module from file."""
|
|
248
|
+
api = self._get_user_api()
|
|
249
|
+
if not api:
|
|
250
|
+
return 1
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
# Import Module class
|
|
254
|
+
from manta.apis.module import Module
|
|
255
|
+
|
|
256
|
+
# Validate file path
|
|
257
|
+
module_path = Path(module_file)
|
|
258
|
+
if not module_path.exists():
|
|
259
|
+
self.print_error(f"Module file not found: {module_file}")
|
|
260
|
+
return 1
|
|
261
|
+
|
|
262
|
+
# Get image name from user if not provided
|
|
263
|
+
default_image = "python:3.9-slim"
|
|
264
|
+
image = Prompt.ask("Container image", default=default_image)
|
|
265
|
+
|
|
266
|
+
# Create Module object
|
|
267
|
+
module = Module(
|
|
268
|
+
python_program=module_path,
|
|
269
|
+
image=image,
|
|
270
|
+
name=module_path.stem if module_path.is_file() else module_path.name,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def run():
|
|
274
|
+
return await api.send_module(module)
|
|
275
|
+
|
|
276
|
+
result_id = self._run_with_progress(
|
|
277
|
+
run, f"Uploading module from {module_file}..."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if result_id is None:
|
|
281
|
+
return 1
|
|
282
|
+
|
|
283
|
+
self.print_success(f"Module uploaded successfully with ID: {result_id}")
|
|
284
|
+
return 0
|
|
285
|
+
|
|
286
|
+
except FileNotFoundError:
|
|
287
|
+
self.print_error(f"Module file not found: {module_file}")
|
|
288
|
+
return 1
|
|
289
|
+
except PermissionError:
|
|
290
|
+
self.print_error(f"Permission denied reading file: {module_file}")
|
|
291
|
+
return 1
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self.print_error(f"Failed to upload module: {e}")
|
|
294
|
+
return 1
|
|
295
|
+
|
|
296
|
+
def update_module(self, module_id: str, module_file: str) -> int:
|
|
297
|
+
"""Update an existing module."""
|
|
298
|
+
api = self._get_user_api()
|
|
299
|
+
if not api:
|
|
300
|
+
return 1
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Import Module class
|
|
304
|
+
from manta.apis.module import Module
|
|
305
|
+
|
|
306
|
+
# Validate module_id parameter
|
|
307
|
+
if not module_id:
|
|
308
|
+
self.print_error("Module ID is required for update operation")
|
|
309
|
+
return 1
|
|
310
|
+
|
|
311
|
+
# Validate file path
|
|
312
|
+
module_path = Path(module_file)
|
|
313
|
+
if not module_path.exists():
|
|
314
|
+
self.print_error(f"Module file not found: {module_file}")
|
|
315
|
+
return 1
|
|
316
|
+
|
|
317
|
+
# First verify the module exists by trying to get it
|
|
318
|
+
async def check_module_exists():
|
|
319
|
+
try:
|
|
320
|
+
return await api.get_module(module_id)
|
|
321
|
+
except Exception:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
existing_module = self._run_with_progress(
|
|
325
|
+
check_module_exists, "Checking if module exists..."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if existing_module is None:
|
|
329
|
+
# Error during fetch, not just missing module
|
|
330
|
+
return 1
|
|
331
|
+
|
|
332
|
+
if not existing_module:
|
|
333
|
+
self.print_error(f"Module with ID '{module_id}' not found")
|
|
334
|
+
return 1
|
|
335
|
+
|
|
336
|
+
# Get image name - use existing image as default or prompt for new one
|
|
337
|
+
existing_image = getattr(existing_module, "image", "python:3.9-slim")
|
|
338
|
+
image = Prompt.ask("Container image", default=existing_image)
|
|
339
|
+
|
|
340
|
+
# Create updated Module object
|
|
341
|
+
module = Module(
|
|
342
|
+
python_program=module_path,
|
|
343
|
+
image=image,
|
|
344
|
+
name=module_path.stem if module_path.is_file() else module_path.name,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
async def run():
|
|
348
|
+
return await api.update_module(module, module_id)
|
|
349
|
+
|
|
350
|
+
result_id = self._run_with_progress(run, f"Updating module {module_id}...")
|
|
351
|
+
|
|
352
|
+
if result_id is None:
|
|
353
|
+
return 1
|
|
354
|
+
|
|
355
|
+
self.print_success(f"Module updated successfully: {result_id}")
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
except FileNotFoundError:
|
|
359
|
+
self.print_error(f"Module file not found: {module_file}")
|
|
360
|
+
return 1
|
|
361
|
+
except PermissionError:
|
|
362
|
+
self.print_error(f"Permission denied reading file: {module_file}")
|
|
363
|
+
return 1
|
|
364
|
+
except Exception as e:
|
|
365
|
+
self.print_error(f"Failed to update module: {e}")
|
|
366
|
+
return 1
|
|
367
|
+
|
|
368
|
+
def delete_module(self, module_id: str, force: bool = False) -> int:
|
|
369
|
+
"""Delete a module."""
|
|
370
|
+
if not force:
|
|
371
|
+
confirm = Confirm.ask(
|
|
372
|
+
f"Are you sure you want to delete module '{module_id}'?"
|
|
373
|
+
)
|
|
374
|
+
if not confirm:
|
|
375
|
+
self.print("Operation cancelled.")
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
api = self._get_user_api()
|
|
379
|
+
if not api:
|
|
380
|
+
return 1
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
|
|
384
|
+
async def run():
|
|
385
|
+
return await api.remove_module(module_id)
|
|
386
|
+
|
|
387
|
+
result = self._run_with_progress(run, "Deleting module...")
|
|
388
|
+
|
|
389
|
+
if result is None:
|
|
390
|
+
return 1
|
|
391
|
+
|
|
392
|
+
self.print_success("Module deleted successfully")
|
|
393
|
+
return 0
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
self.print_error(f"Failed to delete module: {e}")
|
|
397
|
+
return 1
|
|
398
|
+
|
|
399
|
+
def download_module(self, module_id: str, output_file: Optional[str] = None) -> int:
|
|
400
|
+
"""Download a module to file."""
|
|
401
|
+
api = self._get_user_api()
|
|
402
|
+
if not api:
|
|
403
|
+
return 1
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
# Validate module_id parameter
|
|
407
|
+
if not module_id:
|
|
408
|
+
self.print_error("Module ID is required for download operation")
|
|
409
|
+
return 1
|
|
410
|
+
|
|
411
|
+
# Fetch module from server
|
|
412
|
+
async def run():
|
|
413
|
+
return await api.get_module(module_id)
|
|
414
|
+
|
|
415
|
+
module = self._run_with_progress(run, f"Downloading module {module_id}...")
|
|
416
|
+
|
|
417
|
+
if module is None:
|
|
418
|
+
return 1
|
|
419
|
+
|
|
420
|
+
# Check if module has python_files (from Module.from_proto)
|
|
421
|
+
if not hasattr(module, "python_files") or not module.python_files:
|
|
422
|
+
self.print_error("Module contains no python files to download")
|
|
423
|
+
return 1
|
|
424
|
+
|
|
425
|
+
# Determine output path
|
|
426
|
+
if output_file:
|
|
427
|
+
output_path = Path(output_file)
|
|
428
|
+
else:
|
|
429
|
+
# Use module name or ID as default filename
|
|
430
|
+
module_name = getattr(module, "name", None) or module_id
|
|
431
|
+
if len(module.python_files) == 1:
|
|
432
|
+
# Single file - use the filename from the module
|
|
433
|
+
filename = list(module.python_files.keys())[0]
|
|
434
|
+
output_path = Path(filename)
|
|
435
|
+
else:
|
|
436
|
+
# Multiple files - create a directory
|
|
437
|
+
output_path = Path(f"{module_name}_module")
|
|
438
|
+
|
|
439
|
+
# Handle single file vs directory
|
|
440
|
+
if len(module.python_files) == 1:
|
|
441
|
+
# Single file download
|
|
442
|
+
filename, content = next(iter(module.python_files.items()))
|
|
443
|
+
|
|
444
|
+
# If output_file is a directory, put file inside it
|
|
445
|
+
if output_path.is_dir():
|
|
446
|
+
output_path = output_path / filename
|
|
447
|
+
|
|
448
|
+
# Check for conflicts
|
|
449
|
+
if output_path.exists():
|
|
450
|
+
overwrite = Confirm.ask(
|
|
451
|
+
f"File '{output_path}' already exists. Overwrite?"
|
|
452
|
+
)
|
|
453
|
+
if not overwrite:
|
|
454
|
+
self.print("Download cancelled.")
|
|
455
|
+
return 0
|
|
456
|
+
|
|
457
|
+
# Write single file
|
|
458
|
+
try:
|
|
459
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
460
|
+
output_path.write_text(content, encoding="utf-8")
|
|
461
|
+
self.print_success(
|
|
462
|
+
f"Module downloaded to: {output_path.absolute()}"
|
|
463
|
+
)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
self.print_error(f"Failed to write file: {e}")
|
|
466
|
+
return 1
|
|
467
|
+
|
|
468
|
+
else:
|
|
469
|
+
# Multiple files - create directory structure
|
|
470
|
+
if output_path.exists() and output_path.is_dir():
|
|
471
|
+
overwrite = Confirm.ask(
|
|
472
|
+
f"Directory '{output_path}' already exists. Continue?"
|
|
473
|
+
)
|
|
474
|
+
if not overwrite:
|
|
475
|
+
self.print("Download cancelled.")
|
|
476
|
+
return 0
|
|
477
|
+
elif output_path.exists():
|
|
478
|
+
self.print_error(
|
|
479
|
+
f"Path '{output_path}' exists and is not a directory"
|
|
480
|
+
)
|
|
481
|
+
return 1
|
|
482
|
+
|
|
483
|
+
# Create directory and write all files
|
|
484
|
+
try:
|
|
485
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
486
|
+
|
|
487
|
+
files_written = 0
|
|
488
|
+
for filename, content in module.python_files.items():
|
|
489
|
+
file_path = output_path / filename
|
|
490
|
+
|
|
491
|
+
# Create subdirectories if needed
|
|
492
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
493
|
+
|
|
494
|
+
# Write file
|
|
495
|
+
file_path.write_text(content, encoding="utf-8")
|
|
496
|
+
files_written += 1
|
|
497
|
+
|
|
498
|
+
self.print_success(
|
|
499
|
+
f"Module downloaded: {files_written} files written to {output_path.absolute()}"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Show file structure
|
|
503
|
+
tree = Tree(f"📁 {output_path.name}")
|
|
504
|
+
for filename in sorted(module.python_files.keys()):
|
|
505
|
+
tree.add(f"📄 {filename}")
|
|
506
|
+
self.console.print(tree)
|
|
507
|
+
|
|
508
|
+
except Exception as e:
|
|
509
|
+
self.print_error(f"Failed to write files: {e}")
|
|
510
|
+
return 1
|
|
511
|
+
|
|
512
|
+
return 0
|
|
513
|
+
|
|
514
|
+
except Exception as e:
|
|
515
|
+
self.print_error(f"Failed to download module: {e}")
|
|
516
|
+
return 1
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""SDK node information and status commands."""
|
|
2
|
+
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from .base_handler import BaseSDKHandler
|
|
7
|
+
from ..utils import (
|
|
8
|
+
enum_to_string,
|
|
9
|
+
timestamp_to_string,
|
|
10
|
+
extract_platform_type,
|
|
11
|
+
extract_node_resources,
|
|
12
|
+
NODE_STATUS_MAP,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SDKNodesHandler(BaseSDKHandler):
|
|
17
|
+
"""Handle node-related SDK commands."""
|
|
18
|
+
|
|
19
|
+
def add_subparsers(self, parent_parser):
|
|
20
|
+
"""Add node command subparsers."""
|
|
21
|
+
subparsers = parent_parser.add_subparsers(
|
|
22
|
+
dest="node_command", help="Node commands"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# List nodes command
|
|
26
|
+
list_parser = subparsers.add_parser("list", help="List all nodes")
|
|
27
|
+
list_parser.add_argument("--cluster-id", help="Filter by cluster ID")
|
|
28
|
+
|
|
29
|
+
# Get node command
|
|
30
|
+
get_parser = subparsers.add_parser("get", help="Get detailed node information")
|
|
31
|
+
get_parser.add_argument("node_id", help="Node ID to get information for")
|
|
32
|
+
|
|
33
|
+
# Add profile override to all subcommands
|
|
34
|
+
for parser in [list_parser, get_parser]:
|
|
35
|
+
parser.add_argument("--profile", help="Use specific profile")
|
|
36
|
+
|
|
37
|
+
def handle(self, args) -> int:
|
|
38
|
+
"""Handle node commands."""
|
|
39
|
+
if not args.node_command:
|
|
40
|
+
self.print_error("Node command required")
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
if args.node_command == "list":
|
|
44
|
+
return self.list_nodes(getattr(args, "cluster_id", None))
|
|
45
|
+
elif args.node_command == "get":
|
|
46
|
+
return self.get_node(args.node_id)
|
|
47
|
+
else:
|
|
48
|
+
self.print_error(f"Unknown node command: {args.node_command}")
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
def list_nodes(self, cluster_id: str = None) -> int:
|
|
52
|
+
"""List all nodes, optionally filtered by cluster."""
|
|
53
|
+
api = self._get_user_api()
|
|
54
|
+
if not api:
|
|
55
|
+
return 1
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
|
|
59
|
+
async def run():
|
|
60
|
+
if cluster_id:
|
|
61
|
+
return await api.stream_and_fetch_nodes(cluster_id)
|
|
62
|
+
else:
|
|
63
|
+
# Get all nodes across all clusters
|
|
64
|
+
all_nodes = []
|
|
65
|
+
clusters = await api.stream_and_fetch_clusters()
|
|
66
|
+
for cluster in clusters:
|
|
67
|
+
cluster_nodes = await api.stream_and_fetch_nodes(
|
|
68
|
+
cluster["cluster_id"]
|
|
69
|
+
)
|
|
70
|
+
all_nodes.extend(cluster_nodes)
|
|
71
|
+
return all_nodes
|
|
72
|
+
|
|
73
|
+
message = (
|
|
74
|
+
f"Fetching nodes{f' for cluster {cluster_id}' if cluster_id else ''}..."
|
|
75
|
+
)
|
|
76
|
+
nodes = self._run_with_progress(run, message)
|
|
77
|
+
|
|
78
|
+
if not nodes:
|
|
79
|
+
self.print("No nodes found.")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
# Create and display nodes table
|
|
83
|
+
table = Table(
|
|
84
|
+
title=f"Nodes{f' (Cluster: {cluster_id})' if cluster_id else ''}"
|
|
85
|
+
)
|
|
86
|
+
table.add_column("Node ID", style="cyan")
|
|
87
|
+
table.add_column("Status", style="green")
|
|
88
|
+
table.add_column("Type", style="blue")
|
|
89
|
+
table.add_column("Cluster", style="magenta")
|
|
90
|
+
table.add_column("Last Seen", style="yellow")
|
|
91
|
+
|
|
92
|
+
for node in nodes:
|
|
93
|
+
# Use converter functions for consistent parsing
|
|
94
|
+
status = enum_to_string(node.get("node_status", 0), NODE_STATUS_MAP)
|
|
95
|
+
|
|
96
|
+
node_type = extract_platform_type(node.get("platform_info"))
|
|
97
|
+
|
|
98
|
+
last_seen = timestamp_to_string(
|
|
99
|
+
node.get("updated_at") or node.get("created_at")
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
table.add_row(
|
|
103
|
+
node.get("node_id", "Unknown"),
|
|
104
|
+
status,
|
|
105
|
+
node_type,
|
|
106
|
+
cluster_id if cluster_id else "Unknown",
|
|
107
|
+
last_seen,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.console.print(table)
|
|
111
|
+
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.print_error(f"Failed to list nodes: {e}")
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
def get_node(self, node_id: str) -> int:
|
|
119
|
+
"""Get detailed node information."""
|
|
120
|
+
api = self._get_user_api()
|
|
121
|
+
if not api:
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
|
|
126
|
+
async def run():
|
|
127
|
+
# Since get_node needs cluster_id, we need to find which cluster the node belongs to
|
|
128
|
+
clusters = await api.stream_and_fetch_clusters()
|
|
129
|
+
for cluster in clusters:
|
|
130
|
+
nodes = await api.stream_and_fetch_nodes(cluster["cluster_id"])
|
|
131
|
+
for node in nodes:
|
|
132
|
+
if node.get("node_id") == node_id:
|
|
133
|
+
# Found the node, now get its detailed info
|
|
134
|
+
return await api.get_node(cluster["cluster_id"], node_id)
|
|
135
|
+
raise ValueError(f"Node {node_id} not found in any cluster")
|
|
136
|
+
|
|
137
|
+
node = self._run_with_progress(run, f"Fetching node {node_id}...")
|
|
138
|
+
|
|
139
|
+
# Extract and format fields using converters
|
|
140
|
+
status = enum_to_string(node.get("node_status", 0), NODE_STATUS_MAP)
|
|
141
|
+
|
|
142
|
+
node_type = extract_platform_type(node.get("platform_info"))
|
|
143
|
+
|
|
144
|
+
last_seen = timestamp_to_string(
|
|
145
|
+
node.get("updated_at") or node.get("created_at")
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Extract resource information
|
|
149
|
+
resources = extract_node_resources(node.get("metrics_snapshot"))
|
|
150
|
+
|
|
151
|
+
# Display node details
|
|
152
|
+
panel = Panel.fit(
|
|
153
|
+
f"[cyan]Node ID:[/cyan] {node.get('node_id', 'Unknown')}\n"
|
|
154
|
+
f"[cyan]Status:[/cyan] {status}\n"
|
|
155
|
+
f"[cyan]Type:[/cyan] {node_type}\n"
|
|
156
|
+
f"[cyan]Cluster:[/cyan] {node.get('cluster_id', 'Unknown')}\n"
|
|
157
|
+
f"[cyan]Resources:[/cyan] CPU: {resources['cpu_count']}, "
|
|
158
|
+
f"RAM: {resources['memory_gb']}GB\n"
|
|
159
|
+
f"[cyan]Last Seen:[/cyan] {last_seen}",
|
|
160
|
+
title=f"Node: {node_id}",
|
|
161
|
+
)
|
|
162
|
+
self.console.print(panel)
|
|
163
|
+
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self.print_error(f"Failed to get node details: {e}")
|
|
168
|
+
return 1
|