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.
Files changed (54) hide show
  1. manta/__init__.light.py +22 -0
  2. manta/__init__.py +83 -0
  3. manta/__main__.py +21 -0
  4. manta/apis/__init__.py +7 -0
  5. manta/apis/async_user_api.py +6458 -0
  6. manta/apis/graph.py +498 -0
  7. manta/apis/module.py +316 -0
  8. manta/apis/results.py +251 -0
  9. manta/apis/swarm.py +206 -0
  10. manta/apis/user_api.py +1016 -0
  11. manta/cli/__init__.py +1 -0
  12. manta/cli/commands/__init__.py +1 -0
  13. manta/cli/commands/base_handler.py +229 -0
  14. manta/cli/commands/doc.py +192 -0
  15. manta/cli/commands/install.py +346 -0
  16. manta/cli/commands/sdk.py +9 -0
  17. manta/cli/commands/sdk_cluster.py +211 -0
  18. manta/cli/commands/sdk_config.py +347 -0
  19. manta/cli/commands/sdk_globals.py +280 -0
  20. manta/cli/commands/sdk_logs.py +174 -0
  21. manta/cli/commands/sdk_main.py +167 -0
  22. manta/cli/commands/sdk_module.py +516 -0
  23. manta/cli/commands/sdk_nodes.py +168 -0
  24. manta/cli/commands/sdk_original.py +3873 -0
  25. manta/cli/commands/sdk_results.py +265 -0
  26. manta/cli/commands/sdk_swarm.py +454 -0
  27. manta/cli/commands/sdk_user.py +234 -0
  28. manta/cli/commands/status.py +292 -0
  29. manta/cli/component_detector.py +112 -0
  30. manta/cli/config_manager.py +445 -0
  31. manta/cli/main.py +265 -0
  32. manta/cli/utils/__init__.py +27 -0
  33. manta/cli/utils/converters.py +140 -0
  34. manta/clients/cluster_management_client.py +486 -0
  35. manta/clients/local_client.py +149 -0
  36. manta/clients/module_management_client.py +217 -0
  37. manta/clients/swarm_management_client.py +562 -0
  38. manta/clients/user_management_client.py +395 -0
  39. manta/clients/world_client.py +195 -0
  40. manta/light/__init__.py +31 -0
  41. manta/light/globals.py +245 -0
  42. manta/light/local.py +407 -0
  43. manta/light/logging_config.py +39 -0
  44. manta/light/path.py +116 -0
  45. manta/light/results.py +236 -0
  46. manta/light/task.py +100 -0
  47. manta/light/utils.py +217 -0
  48. manta/light/world.py +177 -0
  49. mantatech_sdk-0.5b0.dev65.dist-info/METADATA +1039 -0
  50. mantatech_sdk-0.5b0.dev65.dist-info/RECORD +54 -0
  51. mantatech_sdk-0.5b0.dev65.dist-info/WHEEL +5 -0
  52. mantatech_sdk-0.5b0.dev65.dist-info/entry_points.txt +2 -0
  53. mantatech_sdk-0.5b0.dev65.dist-info/licenses/LICENSE +683 -0
  54. 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