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,347 @@
1
+ """SDK configuration management commands matching node config pattern."""
2
+
3
+ import os
4
+ import subprocess
5
+
6
+ from rich.panel import Panel
7
+ from rich.prompt import Confirm, Prompt, IntPrompt
8
+ from rich.table import Table
9
+
10
+ from .base_handler import BaseSDKHandler
11
+
12
+
13
+ class SDKConfigHandler(BaseSDKHandler):
14
+ """Handle configuration-related SDK commands."""
15
+
16
+ def add_subparsers(self, parent_parser):
17
+ """Add config command subparsers."""
18
+ self.parser = parent_parser # Store parser reference
19
+ subparsers = parent_parser.add_subparsers(
20
+ dest="config_command", help="Config commands"
21
+ )
22
+
23
+ # Init config command
24
+ init_parser = subparsers.add_parser("init", help="Initialize configuration")
25
+ init_parser.add_argument(
26
+ "--interactive", action="store_true", help="Run interactive setup"
27
+ )
28
+ init_parser.add_argument("--name", help="Configuration name", default="default")
29
+ init_parser.add_argument("--token", help="JWT authentication token")
30
+ init_parser.add_argument("--host", default="localhost", help="Server host")
31
+ init_parser.add_argument("--port", type=int, default=50052, help="Server port")
32
+
33
+ # List configs command
34
+ subparsers.add_parser("list", help="List all configurations")
35
+
36
+ # Show config command
37
+ show_parser = subparsers.add_parser("show", help="Show configuration")
38
+ show_parser.add_argument(
39
+ "name", nargs="?", help="Configuration name (uses active if not specified)"
40
+ )
41
+
42
+ # Use config command (set active)
43
+ use_parser = subparsers.add_parser("use", help="Set active configuration")
44
+ use_parser.add_argument("name", help="Configuration name")
45
+
46
+ # Delete config command
47
+ delete_parser = subparsers.add_parser("delete", help="Delete configuration")
48
+ delete_parser.add_argument("name", help="Configuration name")
49
+
50
+ # Edit config command
51
+ edit_parser = subparsers.add_parser(
52
+ "edit", help="Edit configuration file in text editor"
53
+ )
54
+ edit_parser.add_argument(
55
+ "name", nargs="?", help="Configuration name (uses active if not specified)"
56
+ )
57
+
58
+ def handle(self, args) -> int:
59
+ """Handle config commands."""
60
+ if not args.config_command:
61
+ if hasattr(self, "parser"):
62
+ self.parser.print_help()
63
+ return 0 # Return 0 for help display
64
+
65
+ if args.config_command == "init":
66
+ return self.init_config(args)
67
+ elif args.config_command == "list":
68
+ return self.list_configs()
69
+ elif args.config_command == "show":
70
+ return self.show_config(getattr(args, "name", None))
71
+ elif args.config_command == "use":
72
+ return self.use_config(args.name)
73
+ elif args.config_command == "delete":
74
+ return self.delete_config(args.name)
75
+ elif args.config_command == "edit":
76
+ return self.edit_config(getattr(args, "name", None))
77
+ else:
78
+ self.print_error(f"Unknown config command: {args.config_command}")
79
+ return 1
80
+
81
+ # Profile methods removed - using simple config approach
82
+
83
+ def init_config(self, args) -> int:
84
+ """Initialize configuration system."""
85
+ try:
86
+ config_name = getattr(args, "name", "default")
87
+
88
+ # Check if configuration already exists
89
+ config_path = self.config_manager.config_dir / f"{config_name}.toml"
90
+ if config_path.exists():
91
+ overwrite = Confirm.ask(
92
+ f"Configuration '{config_name}' already exists. Overwrite?",
93
+ console=self.console,
94
+ default=False,
95
+ )
96
+ if not overwrite:
97
+ self.print("[yellow]Configuration creation cancelled[/yellow]")
98
+ return 1
99
+
100
+ # Default to interactive mode if no token provided
101
+ token = getattr(args, "token", None)
102
+ interactive = getattr(args, "interactive", False) or token is None
103
+
104
+ if interactive:
105
+ self.console.print(
106
+ Panel(
107
+ "[bold cyan]SDK Configuration Setup[/bold cyan]\n\n"
108
+ "This wizard will help you create a new SDK configuration.\n"
109
+ "Configuration will be saved in ~/.manta/sdk/",
110
+ title="manta sdk Configuration",
111
+ border_style="blue",
112
+ )
113
+ )
114
+
115
+ # Get configuration details
116
+ host = Prompt.ask(
117
+ "[cyan]Manager host[/cyan]",
118
+ console=self.console,
119
+ default="localhost",
120
+ )
121
+
122
+ port = IntPrompt.ask(
123
+ "[cyan]Manager port[/cyan]", console=self.console, default=50052
124
+ )
125
+
126
+ token = Prompt.ask(
127
+ "[cyan]JWT authentication token[/cyan]",
128
+ password=True,
129
+ console=self.console,
130
+ )
131
+
132
+ if not token:
133
+ self.print_error("JWT token is required for SDK authentication")
134
+ return 1
135
+
136
+ # Optional: TLS configuration
137
+ use_tls = Confirm.ask(
138
+ "[cyan]Use TLS connection?[/cyan]",
139
+ console=self.console,
140
+ default=False,
141
+ )
142
+
143
+ cert_folder = None
144
+ if use_tls:
145
+ cert_folder = Prompt.ask(
146
+ "[cyan]Certificate folder path[/cyan]",
147
+ console=self.console,
148
+ default="~/.manta/certs",
149
+ )
150
+ else:
151
+ # Non-interactive mode - use command line arguments
152
+ token = getattr(args, "token", None)
153
+ host = getattr(args, "host", "localhost")
154
+ port = getattr(args, "port", 50052)
155
+ cert_folder = None
156
+
157
+ # Create or update configuration
158
+ if config_path.exists():
159
+ # If we got here, user agreed to overwrite
160
+ success = self.config_manager.update_config(
161
+ name=config_name,
162
+ token=token,
163
+ host=host,
164
+ port=port,
165
+ cert_folder=cert_folder,
166
+ description=f"SDK configuration '{config_name}'",
167
+ )
168
+ else:
169
+ success = self.config_manager.create_config(
170
+ name=config_name,
171
+ token=token,
172
+ host=host,
173
+ port=port,
174
+ cert_folder=cert_folder,
175
+ description=f"SDK configuration '{config_name}'",
176
+ )
177
+
178
+ if success:
179
+ self.config_manager.set_active_config(config_name)
180
+ self.print_success(
181
+ f"Configuration '{config_name}' created and activated successfully"
182
+ )
183
+ if not token:
184
+ self.print("Run 'manta sdk config edit' to add your JWT token")
185
+ else:
186
+ self.print_error("Failed to create configuration")
187
+ return 1
188
+
189
+ return 0
190
+
191
+ except Exception as e:
192
+ self.print_error(f"Failed to initialize configuration: {e}")
193
+ return 1
194
+
195
+ def show_config(self, config_name: str = None) -> int:
196
+ """Show current configuration."""
197
+ try:
198
+ if config_name:
199
+ config = self.config_manager.load_config(config_name)
200
+ if not config:
201
+ self.print_error(f"Configuration '{config_name}' not found")
202
+ return 1
203
+ title = f"Configuration: {config_name}"
204
+ else:
205
+ config = self.config_manager.get_active_config()
206
+ active_name = self.config_manager.get_active_config_name()
207
+ if config:
208
+ title = f"Active Configuration: {active_name}"
209
+ else:
210
+ self.print_error("No active configuration found")
211
+ return 1
212
+
213
+ config_text = ""
214
+ for key, value in config.__dict__.items():
215
+ if key == "token" and value:
216
+ # Mask token for security
217
+ masked_token = value[:8] + "..." if len(value) > 8 else "***"
218
+ config_text += f"[cyan]{key}:[/cyan] {masked_token}\n"
219
+ else:
220
+ config_text += f"[cyan]{key}:[/cyan] {value}\n"
221
+
222
+ panel = Panel.fit(config_text.strip(), title=title)
223
+ self.console.print(panel)
224
+
225
+ return 0
226
+
227
+ except Exception as e:
228
+ self.print_error(f"Failed to show configuration: {e}")
229
+ return 1
230
+
231
+ def edit_config(self, config_name: str = None) -> int:
232
+ """Edit configuration file in default editor."""
233
+ try:
234
+ if config_name is None:
235
+ config_name = self.config_manager.get_active_config_name()
236
+
237
+ config_path = self.config_manager.config_dir / f"{config_name}.toml"
238
+ if not config_path.exists():
239
+ self.print_error(f"Configuration '{config_name}' not found")
240
+ return 1
241
+
242
+ # Get default editor
243
+ editor = os.environ.get("EDITOR", "nano")
244
+
245
+ # Launch editor
246
+ try:
247
+ subprocess.run([editor, str(config_path)], check=True)
248
+ self.print_success(f"Configuration '{config_name}' edited successfully")
249
+ return 0
250
+ except subprocess.CalledProcessError:
251
+ self.print_error(f"Failed to launch editor: {editor}")
252
+ return 1
253
+ except FileNotFoundError:
254
+ self.print_error(f"Editor not found: {editor}")
255
+ self.print(
256
+ "Set the EDITOR environment variable to your preferred editor"
257
+ )
258
+ return 1
259
+
260
+ except Exception as e:
261
+ self.print_error(f"Failed to edit configuration: {e}")
262
+ return 1
263
+
264
+ def list_configs(self) -> int:
265
+ """List all configurations."""
266
+ try:
267
+ configs = self.config_manager.list_configs()
268
+ active_config_name = self.config_manager.get_active_config_name()
269
+
270
+ if not configs:
271
+ self.print("No configurations found.")
272
+ self.print("Run 'manta sdk config init --interactive' to create one.")
273
+ return 0
274
+
275
+ table = Table(title="SDK Configurations")
276
+ table.add_column("Configuration Name", style="cyan")
277
+ table.add_column("Status", style="green")
278
+ table.add_column("Host", style="blue")
279
+ table.add_column("Port", style="magenta")
280
+ table.add_column("Description", style="white")
281
+
282
+ for config_name in configs:
283
+ config = self.config_manager.load_config(config_name)
284
+ status = "Active" if config_name == active_config_name else ""
285
+
286
+ if config:
287
+ table.add_row(
288
+ config_name,
289
+ status,
290
+ config.host,
291
+ str(config.port),
292
+ config.description or "",
293
+ )
294
+ else:
295
+ table.add_row(
296
+ config_name, status, "Error", "Error", "Failed to load"
297
+ )
298
+
299
+ self.console.print(table)
300
+
301
+ return 0
302
+
303
+ except Exception as e:
304
+ self.print_error(f"Failed to list configurations: {e}")
305
+ return 1
306
+
307
+ def use_config(self, config_name: str) -> int:
308
+ """Set the active configuration."""
309
+ try:
310
+ success = self.config_manager.set_active_config(config_name)
311
+ if success:
312
+ self.print_success(f"Active configuration set to '{config_name}'")
313
+ return 0
314
+ else:
315
+ return 1
316
+
317
+ except Exception as e:
318
+ self.print_error(f"Failed to set active configuration: {e}")
319
+ return 1
320
+
321
+ def delete_config(self, config_name: str) -> int:
322
+ """Delete a configuration."""
323
+ try:
324
+ if config_name == "default":
325
+ self.print_error("Cannot delete default configuration")
326
+ return 1
327
+
328
+ confirm = Confirm.ask(
329
+ f"Are you sure you want to delete configuration '{config_name}'?",
330
+ console=self.console,
331
+ )
332
+ if not confirm:
333
+ self.print("Operation cancelled.")
334
+ return 0
335
+
336
+ success = self.config_manager.delete_config(config_name)
337
+ if success:
338
+ self.print_success(
339
+ f"Configuration '{config_name}' deleted successfully"
340
+ )
341
+ return 0
342
+ else:
343
+ return 1
344
+
345
+ except Exception as e:
346
+ self.print_error(f"Failed to delete configuration: {e}")
347
+ return 1
@@ -0,0 +1,280 @@
1
+ """SDK globals retrieval and management commands."""
2
+
3
+ import json
4
+ import signal
5
+ import time
6
+
7
+ from rich.table import Table
8
+
9
+ from .base_handler import BaseSDKHandler
10
+
11
+
12
+ class SDKGlobalsHandler(BaseSDKHandler):
13
+ """Handle globals-related SDK commands."""
14
+
15
+ def add_subparsers(self, parent_parser):
16
+ """Add globals command subparsers."""
17
+ self.parser = parent_parser # Store parser reference
18
+ subparsers = parent_parser.add_subparsers(
19
+ dest="globals_command", help="Globals commands"
20
+ )
21
+
22
+ # List tags command
23
+ list_tags_parser = subparsers.add_parser(
24
+ "list-tags", help="List available global tags for a swarm"
25
+ )
26
+ list_tags_parser.add_argument(
27
+ "swarm_id", help="Swarm ID to list global tags for"
28
+ )
29
+
30
+ # Get globals command
31
+ get_parser = subparsers.add_parser("get", help="Get globals for a swarm")
32
+ get_parser.add_argument("swarm_id", help="Swarm ID to get globals for")
33
+ get_parser.add_argument(
34
+ "--tag", default="latest", help="Global tag (default: latest)"
35
+ )
36
+
37
+ # Export globals command
38
+ export_parser = subparsers.add_parser("export", help="Export globals to file")
39
+ export_parser.add_argument("swarm_id", help="Swarm ID to export globals for")
40
+ export_parser.add_argument(
41
+ "--tag", default="latest", help="Global tag (default: latest)"
42
+ )
43
+ export_parser.add_argument("--output", required=True, help="Output file path")
44
+
45
+ # Stream globals command
46
+ stream_parser = subparsers.add_parser(
47
+ "stream", help="Stream globals in real-time"
48
+ )
49
+ stream_parser.add_argument("swarm_id", help="Swarm ID to stream globals for")
50
+ stream_parser.add_argument(
51
+ "--tag", default="latest", help="Global tag (default: latest)"
52
+ )
53
+
54
+ # Add profile override to all subcommands
55
+ for parser in [list_tags_parser, get_parser, export_parser, stream_parser]:
56
+ parser.add_argument("--profile", help="Use specific profile")
57
+
58
+ def handle(self, args) -> int:
59
+ """Handle globals commands."""
60
+ if not args.globals_command:
61
+ if hasattr(self, "parser"):
62
+ self.parser.print_help()
63
+ return 0 # Return 0 for help display
64
+
65
+ if args.globals_command == "list-tags":
66
+ return self.list_tags(args.swarm_id)
67
+ elif args.globals_command == "get":
68
+ return self.get_globals(args.swarm_id, getattr(args, "tag", "latest"))
69
+ elif args.globals_command == "export":
70
+ return self.export_globals(
71
+ args.swarm_id, getattr(args, "tag", "latest"), args.output
72
+ )
73
+ elif args.globals_command == "stream":
74
+ return self.stream_globals(args.swarm_id, getattr(args, "tag", "latest"))
75
+ else:
76
+ self.print_error(f"Unknown globals command: {args.globals_command}")
77
+ return 1
78
+
79
+ def list_tags(self, swarm_id: str) -> int:
80
+ """List available global tags for a swarm."""
81
+ api = self._get_user_api()
82
+ if not api:
83
+ return 1
84
+
85
+ try:
86
+
87
+ async def run():
88
+ return await api.list_global_tags(swarm_id)
89
+
90
+ tags_info = self._run_with_progress(
91
+ run, f"Fetching global tags for swarm {swarm_id}..."
92
+ )
93
+
94
+ if tags_info is None:
95
+ return 1
96
+
97
+ if not tags_info or not tags_info.get("tags"):
98
+ self.print("No global tags found for this swarm.")
99
+ return 0
100
+
101
+ # Display tags in a table
102
+ table = Table(title=f"Global Tags for Swarm: {swarm_id}")
103
+ table.add_column("Tag", style="cyan")
104
+ table.add_column("Count", style="green")
105
+ table.add_column("Total Size", style="yellow")
106
+ table.add_column("Last Updated", style="blue")
107
+
108
+ tags = tags_info.get("tags", {})
109
+ for tag, tag_data in tags.items():
110
+ count = tag_data.get("count", 0)
111
+ size = tag_data.get("total_size", 0)
112
+ last_updated = tag_data.get("last_updated", "Unknown")
113
+
114
+ table.add_row(tag, str(count), self._format_size(size), last_updated)
115
+
116
+ self.console.print(table)
117
+
118
+ return 0
119
+
120
+ except Exception as e:
121
+ self.print_error(f"Failed to list global tags: {e}")
122
+ return 1
123
+
124
+ def get_globals(self, swarm_id: str, tag: str = "latest") -> int:
125
+ """Get globals for a swarm."""
126
+ api = self._get_user_api()
127
+ if not api:
128
+ return 1
129
+
130
+ try:
131
+
132
+ async def run():
133
+ # Use select_global to get globals data
134
+ globals_data = []
135
+ async for global_item in api.select_global(swarm_id, tag):
136
+ globals_data.append(global_item)
137
+ return globals_data
138
+
139
+ globals_data = self._run_with_progress(
140
+ run, f"Fetching globals for swarm {swarm_id}..."
141
+ )
142
+
143
+ if globals_data is None:
144
+ return 1
145
+
146
+ if not globals_data:
147
+ self.print("No globals found.")
148
+ return 0
149
+
150
+ # Display globals
151
+ table = Table(title=f"Globals for Swarm: {swarm_id} (Tag: {tag})")
152
+ table.add_column("Global ID", style="cyan")
153
+ table.add_column("Type", style="blue")
154
+ table.add_column("Size", style="green")
155
+ table.add_column("Created", style="yellow")
156
+
157
+ for global_item in globals_data:
158
+ data = global_item.get("data", {})
159
+ data_size = len(json.dumps(data)) if data else 0
160
+
161
+ table.add_row(
162
+ global_item.get("global_id", "Unknown"),
163
+ global_item.get("global_type", "Unknown"),
164
+ self._format_size(data_size),
165
+ global_item.get("created_at", "Unknown"),
166
+ )
167
+
168
+ self.console.print(table)
169
+
170
+ # Show first few globals as samples
171
+ self.print("\nSample Globals:")
172
+ for i, global_item in enumerate(globals_data[:3]):
173
+ self.print(f"Global {i + 1}:")
174
+ self.print(json.dumps(global_item.get("data", {}), indent=2))
175
+ if i < 2 and i < len(globals_data) - 1:
176
+ self.print("-" * 40)
177
+
178
+ return 0
179
+
180
+ except Exception as e:
181
+ self.print_error(f"Failed to get globals: {e}")
182
+ return 1
183
+
184
+ def export_globals(self, swarm_id: str, tag: str, output_file: str) -> int:
185
+ """Export globals to a file."""
186
+ api = self._get_user_api()
187
+ if not api:
188
+ return 1
189
+
190
+ try:
191
+
192
+ async def run():
193
+ # Use select_global to get globals data
194
+ globals_data = []
195
+ async for global_item in api.select_global(swarm_id, tag):
196
+ globals_data.append(global_item)
197
+ return globals_data
198
+
199
+ globals_data = self._run_with_progress(
200
+ run, "Fetching globals for export..."
201
+ )
202
+
203
+ if globals_data is None:
204
+ return 1
205
+
206
+ if not globals_data:
207
+ self.print_error("No globals found")
208
+ return 1
209
+
210
+ # Write to file
211
+ with open(output_file, "w") as f:
212
+ json.dump(globals_data, f, indent=2)
213
+
214
+ self.print_success(f"Globals exported to {output_file}")
215
+ return 0
216
+
217
+ except Exception as e:
218
+ self.print_error(f"Export failed: {e}")
219
+ return 1
220
+
221
+ def stream_globals(self, swarm_id: str, tag: str = "latest") -> int:
222
+ """Stream globals in real-time."""
223
+ api = self._get_user_api()
224
+ if not api:
225
+ return 1
226
+
227
+ self.print(f"Streaming globals for swarm: {swarm_id} (Tag: {tag})")
228
+ self.print("Press Ctrl+C to stop streaming...")
229
+
230
+ try:
231
+ # Set up signal handler for graceful shutdown
232
+ shutdown_flag = False
233
+
234
+ def signal_handler(sig, frame):
235
+ nonlocal shutdown_flag
236
+ shutdown_flag = True
237
+
238
+ signal.signal(signal.SIGINT, signal_handler)
239
+
240
+ last_global_count = 0
241
+
242
+ while not shutdown_flag:
243
+ try:
244
+
245
+ async def get_globals():
246
+ globals_data = []
247
+ async for global_item in api.select_global(swarm_id, tag):
248
+ globals_data.append(global_item)
249
+ return globals_data
250
+
251
+ globals_data = self._run_async(get_globals())
252
+
253
+ # Show new globals since last check
254
+ if globals_data and len(globals_data) > last_global_count:
255
+ new_globals = globals_data[last_global_count:]
256
+
257
+ for global_item in new_globals:
258
+ timestamp = global_item.get("created_at", "unknown")
259
+ data = global_item.get("data", {})
260
+ self.print(f"[{timestamp}] {json.dumps(data, indent=2)}")
261
+
262
+ last_global_count = len(globals_data)
263
+
264
+ time.sleep(5) # Wait 5 seconds before next poll
265
+
266
+ except KeyboardInterrupt:
267
+ self.print("\nStreaming stopped.")
268
+ return 130 # KeyboardInterrupt exit code
269
+
270
+ except Exception as e:
271
+ self.print_error(f"Streaming failed: {e}")
272
+ return 1
273
+
274
+ def _format_size(self, size: int) -> str:
275
+ """Format byte size to human readable string."""
276
+ for unit in ["B", "KB", "MB", "GB"]:
277
+ if size < 1024.0:
278
+ return f"{size:.1f} {unit}"
279
+ size /= 1024.0
280
+ return f"{size:.1f} TB"