manta-node 0.5b0__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 (43) hide show
  1. manta_node/__init__.py +5 -0
  2. manta_node/__main__.py +57 -0
  3. manta_node/cli/__init__.py +6 -0
  4. manta_node/cli/commands/__init__.py +21 -0
  5. manta_node/cli/commands/cluster.py +419 -0
  6. manta_node/cli/commands/config.py +490 -0
  7. manta_node/cli/commands/logs.py +168 -0
  8. manta_node/cli/commands/start.py +459 -0
  9. manta_node/cli/commands/status.py +204 -0
  10. manta_node/cli/commands/stop.py +253 -0
  11. manta_node/cli/config_manager.py +139 -0
  12. manta_node/cli/main.py +133 -0
  13. manta_node/cli/version.py +106 -0
  14. manta_node/domain/__init__.py +8 -0
  15. manta_node/domain/task_lifecycle.py +90 -0
  16. manta_node/infrastructure/__init__.py +13 -0
  17. manta_node/infrastructure/config/__init__.py +31 -0
  18. manta_node/infrastructure/config/node_config_manager.py +918 -0
  19. manta_node/infrastructure/container/__init__.py +11 -0
  20. manta_node/infrastructure/container/docker_adapter.py +253 -0
  21. manta_node/infrastructure/container/image_executor.py +222 -0
  22. manta_node/infrastructure/container/manager.py +549 -0
  23. manta_node/infrastructure/filesystem/__init__.py +7 -0
  24. manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
  25. manta_node/infrastructure/grpc/__init__.py +11 -0
  26. manta_node/infrastructure/grpc/client.py +373 -0
  27. manta_node/infrastructure/grpc/local_servicer.py +151 -0
  28. manta_node/infrastructure/grpc/world_servicer.py +284 -0
  29. manta_node/infrastructure/metrics/__init__.py +10 -0
  30. manta_node/infrastructure/metrics/collector.py +94 -0
  31. manta_node/infrastructure/metrics/metrics_collector.py +351 -0
  32. manta_node/infrastructure/mqtt/__init__.py +7 -0
  33. manta_node/infrastructure/mqtt/command_handler.py +450 -0
  34. manta_node/node_orchestrator.py +462 -0
  35. manta_node/task_manager.py +677 -0
  36. manta_node/tasks.py +273 -0
  37. manta_node/utils.py +52 -0
  38. manta_node-0.5b0.dist-info/METADATA +794 -0
  39. manta_node-0.5b0.dist-info/RECORD +43 -0
  40. manta_node-0.5b0.dist-info/WHEEL +5 -0
  41. manta_node-0.5b0.dist-info/entry_points.txt +2 -0
  42. manta_node-0.5b0.dist-info/licenses/LICENSE +683 -0
  43. manta_node-0.5b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,253 @@
1
+ """Node stop command implementation."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import signal
7
+ import time
8
+ from pathlib import Path
9
+ from typing import List
10
+
11
+ from rich.console import Console
12
+
13
+ console = Console()
14
+
15
+
16
+ def print_error(message: str):
17
+ """Print error message in red."""
18
+ console.print(f"[red]Error: {message}[/red]")
19
+
20
+
21
+ def print_success(message: str):
22
+ """Print success message in green."""
23
+ console.print(f"[green]{message}[/green]")
24
+
25
+
26
+ def print_warning(message: str):
27
+ """Print warning message in yellow."""
28
+ console.print(f"[yellow]{message}[/yellow]")
29
+
30
+
31
+ def find_running_instances():
32
+ """Find running node instances by looking for instance JSON files."""
33
+
34
+ instances_dir = Path.home() / ".manta" / "nodes" / "instances"
35
+ instances = []
36
+
37
+ if not instances_dir.exists():
38
+ return instances
39
+
40
+ for instance_file in instances_dir.glob("*.json"):
41
+ try:
42
+ with open(instance_file, "r") as f:
43
+ instance_data = json.load(f)
44
+
45
+ pid = instance_data.get("pid")
46
+ instance_id = instance_data.get("instance_id", instance_file.stem)
47
+ alias = instance_data.get("alias", instance_id)
48
+
49
+ # Check if process is still running
50
+ try:
51
+ # Send signal 0 to check if process exists
52
+
53
+ os.kill(pid, 0)
54
+ instances.append(
55
+ {
56
+ "instance_id": instance_id,
57
+ "alias": alias,
58
+ "pid": pid,
59
+ "instance_file": instance_file,
60
+ }
61
+ )
62
+ except OSError:
63
+ # Process no longer exists, remove stale instance file
64
+ instance_file.unlink(missing_ok=True)
65
+
66
+ except (ValueError, FileNotFoundError):
67
+ # Invalid instance file, remove it
68
+ instance_file.unlink(missing_ok=True)
69
+
70
+ return instances
71
+
72
+
73
+ def stop_instance_by_pid(
74
+ instance: dict, force: bool = False, timeout: int = 10
75
+ ) -> bool:
76
+ """Stop a node instance using its PID.
77
+
78
+ Args:
79
+ instance: Instance information dictionary
80
+ force: If True, use SIGKILL instead of SIGTERM
81
+ timeout: Seconds to wait for graceful shutdown before force killing
82
+
83
+ Returns:
84
+ True if successfully stopped, False otherwise
85
+ """
86
+ pid = instance["pid"]
87
+ instance_id = instance["instance_id"]
88
+ alias = instance.get("alias", instance_id)
89
+
90
+ try:
91
+ if force:
92
+ # Force kill with SIGKILL
93
+ os.kill(pid, signal.SIGKILL)
94
+ console.print(f"Force killed node '{alias}' (PID: {pid})")
95
+ else:
96
+ # Graceful shutdown with SIGTERM
97
+ os.kill(pid, signal.SIGTERM)
98
+ console.print(f"Sent termination signal to node '{alias}' (PID: {pid})")
99
+
100
+ # Wait for process to terminate gracefully
101
+ console.print(
102
+ f"[dim]Waiting up to {timeout} seconds for graceful shutdown...[/dim]"
103
+ )
104
+ start_time = time.time()
105
+ while time.time() - start_time < timeout:
106
+ try:
107
+ # Check if process is still running
108
+ os.kill(pid, 0)
109
+ time.sleep(0.5)
110
+ except OSError:
111
+ # Process has terminated
112
+ console.print(f"[green]Node '{alias}' stopped gracefully[/green]")
113
+ break
114
+ else:
115
+ # Timeout reached, process still running
116
+ print_warning(
117
+ f"Node '{alias}' did not stop gracefully, sending SIGKILL..."
118
+ )
119
+ os.kill(pid, signal.SIGKILL)
120
+ console.print(f"Force killed node '{alias}' after timeout")
121
+
122
+ # Remove instance file
123
+ instance["instance_file"].unlink(missing_ok=True)
124
+ return True
125
+
126
+ except OSError as e:
127
+ if e.errno == 3: # No such process
128
+ # Process already dead, just remove instance file
129
+ instance["instance_file"].unlink(missing_ok=True)
130
+ print_warning(f"Node '{alias}' was already stopped")
131
+ return True
132
+ else:
133
+ print_error(f"Failed to stop node '{alias}': {e}")
134
+ return False
135
+
136
+
137
+ def stop_all_instances(force: bool = False, timeout: int = 10) -> int:
138
+ """Stop all running node instances."""
139
+ instances = find_running_instances()
140
+
141
+ if not instances:
142
+ console.print("No running node instances found.")
143
+ return 0
144
+
145
+ console.print(f"Found {len(instances)} running instance(s):")
146
+ for instance in instances:
147
+ console.print(f" - {instance['alias']} (PID: {instance['pid']})")
148
+
149
+ console.print("")
150
+ if force:
151
+ console.print("Force stopping all instances...")
152
+ else:
153
+ console.print("Stopping all instances...")
154
+
155
+ success_count = 0
156
+ for instance in instances:
157
+ if stop_instance_by_pid(instance, force, timeout):
158
+ success_count += 1
159
+
160
+ if success_count == len(instances):
161
+ print_success(f"All {success_count} instances stopped successfully")
162
+ return 0
163
+ else:
164
+ print_error(
165
+ f"Only {success_count}/{len(instances)} instances stopped successfully"
166
+ )
167
+ return 1
168
+
169
+
170
+ def stop_specific_instance(
171
+ instance_name: str, force: bool = False, timeout: int = 10
172
+ ) -> int:
173
+ """Stop a specific node instance by name or ID."""
174
+ instances = find_running_instances()
175
+
176
+ if not instances:
177
+ print_error("No running node instances found")
178
+ return 1
179
+
180
+ # Find matching instance (by alias or instance_id)
181
+ target_instance = None
182
+ for instance in instances:
183
+ if (
184
+ instance["alias"] == instance_name
185
+ or instance["instance_id"] == instance_name
186
+ or instance["instance_id"].startswith(instance_name)
187
+ ):
188
+ target_instance = instance
189
+ break
190
+
191
+ if not target_instance:
192
+ print_error(f"Node instance '{instance_name}' not found")
193
+ console.print("Running instances:")
194
+ for instance in instances:
195
+ console.print(f" - {instance['alias']} (PID: {instance['pid']})")
196
+ return 1
197
+
198
+ if stop_instance_by_pid(target_instance, force, timeout):
199
+ print_success(f"Node '{target_instance['alias']}' stopped successfully")
200
+ return 0
201
+ else:
202
+ return 1
203
+
204
+
205
+ def stop_command(args: List[str]) -> int:
206
+ """Handle node stop command.
207
+
208
+ Args:
209
+ args: Command line arguments
210
+
211
+ Returns:
212
+ Exit code (0 for success, non-zero for error)
213
+ """
214
+ parser = argparse.ArgumentParser(
215
+ prog="manta_node stop", description="Stop running node instances"
216
+ )
217
+ parser.add_argument(
218
+ "instance",
219
+ nargs="?",
220
+ help="Instance ID or name to stop (if not provided, stops all instances)",
221
+ )
222
+ parser.add_argument("--all", action="store_true", help="Stop all running instances")
223
+ parser.add_argument(
224
+ "--force",
225
+ "-f",
226
+ action="store_true",
227
+ help="Force stop using SIGKILL instead of SIGTERM",
228
+ )
229
+ parser.add_argument(
230
+ "--timeout",
231
+ "-t",
232
+ type=int,
233
+ default=10,
234
+ help="Timeout in seconds to wait for graceful shutdown (default: 10)",
235
+ )
236
+
237
+ try:
238
+ parsed_args = parser.parse_args(args)
239
+ except SystemExit:
240
+ return 1
241
+
242
+ if parsed_args.all or not parsed_args.instance:
243
+ return stop_all_instances(parsed_args.force, parsed_args.timeout)
244
+ else:
245
+ return stop_specific_instance(
246
+ parsed_args.instance, parsed_args.force, parsed_args.timeout
247
+ )
248
+
249
+
250
+ if __name__ == "__main__":
251
+ import sys
252
+
253
+ sys.exit(stop_command(sys.argv[1:]))
@@ -0,0 +1,139 @@
1
+ """
2
+ Compatibility layer for old configuration classes.
3
+
4
+ This module provides compatibility classes that match the old configuration API
5
+ but delegate to the new NodeConfiguration system.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Any, Dict, Optional
10
+
11
+ from manta_node.infrastructure.config.node_config_manager import IdentityConfig
12
+
13
+ from ..infrastructure.config import NodeConfiguration
14
+
15
+
16
+ @dataclass
17
+ class ConnectionConfig:
18
+ """Compatibility class for old ConnectionConfig."""
19
+
20
+ host: str = "localhost"
21
+ port: int = 50051
22
+ use_tls: bool = False
23
+ cert_folder: Optional[str] = None
24
+
25
+
26
+ @dataclass
27
+ class HardwareConfig:
28
+ """Compatibility class for old HardwareConfig."""
29
+
30
+ gpu_enabled: Optional[bool] = None
31
+ max_concurrent_tasks: int = 2
32
+ cpu_limit: str = "2.0"
33
+ memory_limit: str = "4G"
34
+
35
+ def get(self, key: str, default=None):
36
+ """Make this object behave like a dictionary for compatibility."""
37
+ return getattr(self, key, default)
38
+
39
+ def copy(self):
40
+ """Return a copy of this hardware config."""
41
+ return HardwareConfig(
42
+ gpu_enabled=self.gpu_enabled,
43
+ max_concurrent_tasks=self.max_concurrent_tasks,
44
+ cpu_limit=self.cpu_limit,
45
+ memory_limit=self.memory_limit,
46
+ )
47
+
48
+
49
+ @dataclass
50
+ class NodeConfig:
51
+ """Compatibility class for old NodeConfig API."""
52
+
53
+ name: str
54
+ alias: str
55
+ connection: ConnectionConfig
56
+ hardware: Optional[HardwareConfig] = None
57
+ datasets: Optional[Dict[str, str]] = None
58
+ auto_register: bool = True
59
+ random_id: bool = False
60
+ created_at: Optional[Any] = None
61
+ updated_at: Optional[Any] = None
62
+
63
+ def __post_init__(self):
64
+ """Ensure datasets is a proper dict if provided."""
65
+ if self.datasets is None:
66
+ self.datasets = {}
67
+ # Set default timestamps if not provided
68
+ from datetime import datetime
69
+
70
+ if self.created_at is None:
71
+ self.created_at = datetime.now()
72
+ if self.updated_at is None:
73
+ self.updated_at = datetime.now()
74
+
75
+ def to_node_configuration(self) -> NodeConfiguration:
76
+ """Convert to new NodeConfiguration format."""
77
+ config = NodeConfiguration(
78
+ identity=IdentityConfig(
79
+ alias=self.alias, random_id=self.random_id, secured_token=""
80
+ )
81
+ )
82
+
83
+ # Metadata
84
+ config.metadata.name = self.name
85
+
86
+ # Identity
87
+ config.identity.alias = self.alias
88
+ config.identity.random_id = self.random_id
89
+
90
+ # Network
91
+ config.network.manager_host = self.connection.host
92
+ config.network.manager_port = self.connection.port
93
+
94
+ # Security
95
+ config.security.use_tls = self.connection.use_tls
96
+ if self.connection.cert_folder:
97
+ config.security.cert_folder = self.connection.cert_folder
98
+
99
+ # Containers/Hardware
100
+ if self.hardware:
101
+ config.containers.gpu_enabled = self.hardware.gpu_enabled
102
+ config.containers.default_cpu_limit = self.hardware.cpu_limit
103
+ config.containers.default_memory_limit = self.hardware.memory_limit
104
+ config.tasks.max_concurrent = self.hardware.max_concurrent_tasks
105
+
106
+ # Datasets
107
+ if self.datasets:
108
+ config.datasets.mappings = self.datasets
109
+
110
+ return config
111
+
112
+ @classmethod
113
+ def from_node_configuration(cls, config: NodeConfiguration) -> "NodeConfig":
114
+ """Create from new NodeConfiguration format."""
115
+ connection = ConnectionConfig(
116
+ host=config.network.manager_host,
117
+ port=config.network.manager_port,
118
+ use_tls=config.security.use_tls,
119
+ cert_folder=config.security.cert_folder,
120
+ )
121
+
122
+ hardware = HardwareConfig(
123
+ gpu_enabled=config.containers.gpu_enabled,
124
+ max_concurrent_tasks=config.tasks.max_concurrent,
125
+ cpu_limit=config.containers.default_cpu_limit,
126
+ memory_limit=config.containers.default_memory_limit,
127
+ )
128
+
129
+ return cls(
130
+ name=config.metadata.name,
131
+ alias=config.identity.alias or config.metadata.name,
132
+ connection=connection,
133
+ hardware=hardware,
134
+ datasets=(
135
+ config.datasets.mappings.copy() if config.datasets.mappings else None
136
+ ),
137
+ auto_register=True, # Default assumption
138
+ random_id=config.identity.random_id,
139
+ )
manta_node/cli/main.py ADDED
@@ -0,0 +1,133 @@
1
+ """Manta Node CLI main entry point."""
2
+
3
+ import sys
4
+ from typing import List, Optional
5
+
6
+ from rich.console import Console
7
+
8
+ from .commands.cluster import cluster_command
9
+ from .commands.config import config_command
10
+ from .commands.logs import logs_command
11
+ from .commands.start import start_command
12
+ from .commands.status import status_command
13
+ from .commands.stop import stop_command
14
+ from .version import get_version_string, get_full_version_info
15
+
16
+ console = Console()
17
+
18
+
19
+ def show_version(verbose: bool = False):
20
+ """Show version information."""
21
+ if verbose:
22
+ info = get_full_version_info()
23
+ console.print(
24
+ f"[bold cyan]Manta Node[/bold cyan] version [green]{info['version']}[/green]"
25
+ )
26
+ console.print(f" Package version: {info['package_version']}")
27
+ console.print(f" Build date: {info['date']}")
28
+ console.print(f" Commit ID: {info['commit']}")
29
+ else:
30
+ console.print(
31
+ f"[bold cyan]manta_node[/bold cyan] [green]{get_version_string()}[/green]"
32
+ )
33
+
34
+
35
+ def show_help():
36
+ """Show CLI help information."""
37
+ help_text = """
38
+ [bold cyan]Manta Node CLI[/bold cyan]
39
+
40
+ [bold]Usage:[/bold]
41
+ manta_node <command> [options]
42
+
43
+ [bold]Commands:[/bold]
44
+ [cyan]start[/cyan] Start a node instance
45
+ [cyan]stop[/cyan] Stop running node instances
46
+ [cyan]status[/cyan] Show status of running nodes
47
+ [cyan]logs[/cyan] View node logs
48
+ [cyan]config[/cyan] Manage node configurations
49
+ [cyan]cluster[/cyan] Manage node clusters
50
+ [cyan]version[/cyan] Show version information
51
+ [cyan]help[/cyan] Show this help message
52
+
53
+ [bold]Global Options:[/bold]
54
+ [cyan]-v, --version[/cyan] Show version information
55
+ [cyan]-h, --help[/cyan] Show this help message
56
+
57
+ [bold]Examples:[/bold]
58
+ manta_node start --config default
59
+ manta_node status --all
60
+ manta_node logs --follow node-123
61
+ manta_node config init <path/to/config.yaml>
62
+ manta_node --version
63
+
64
+ For more help on a specific command:
65
+ manta_node <command> --help
66
+ """
67
+ console.print(help_text)
68
+
69
+
70
+ def main(args: Optional[List[str]] = None) -> int:
71
+ """Main CLI entry point for manta-node.
72
+
73
+ Args:
74
+ args: Optional command line arguments (uses sys.argv if None)
75
+
76
+ Returns:
77
+ Exit code (0 for success, non-zero for error)
78
+ """
79
+ if args is None:
80
+ args = sys.argv[1:] # Skip the script name
81
+
82
+ # Remove 'node' if it's the first argument (when called as 'manta_node <command>')
83
+ if args and args[0] == "node":
84
+ args = args[1:]
85
+
86
+ # Show version if requested
87
+ if args and args[0] in ["-v", "--version", "version"]:
88
+ # Check for verbose flag
89
+ verbose = len(args) > 1 and args[1] in ["-v", "--verbose"]
90
+ show_version(verbose=verbose)
91
+ return 0
92
+
93
+ # Show help if no arguments or help requested
94
+ if not args or args[0] in ["-h", "--help", "help"]:
95
+ show_help()
96
+ return 0
97
+
98
+ command = args[0]
99
+ command_args = args[1:]
100
+
101
+ try:
102
+ # Import and execute the appropriate command
103
+ if command == "start":
104
+ return start_command(command_args)
105
+ elif command == "stop":
106
+ return stop_command(command_args)
107
+ elif command == "status":
108
+ return status_command(command_args)
109
+ elif command == "logs":
110
+ return logs_command(command_args)
111
+ elif command == "config":
112
+ return config_command(command_args)
113
+ elif command == "cluster":
114
+ return cluster_command(command_args)
115
+ else:
116
+ console.print(f"[red]Error: Unknown command '{command}'[/red]")
117
+ console.print("\nUse 'manta_node help' to see available commands.")
118
+ return 1
119
+
120
+ except ImportError as e:
121
+ console.print(f"[red]Error: Command '{command}' not available[/red]")
122
+ console.print(f"[dim]{e}[/dim]")
123
+ return 1
124
+ except KeyboardInterrupt:
125
+ console.print("\n[yellow]Operation cancelled by user[/yellow]")
126
+ return 1
127
+ except Exception as e:
128
+ console.print(f"[red]Unexpected error: {e}[/red]")
129
+ return 1
130
+
131
+
132
+ if __name__ == "__main__":
133
+ sys.exit(main())
@@ -0,0 +1,106 @@
1
+ """Version information for Manta Node CLI.
2
+
3
+ Version information is loaded from:
4
+ 1. _version_generated.py (created at build time by scripts/generate_version.py)
5
+ 2. Fall back to dynamic git detection if not available (development mode)
6
+ """
7
+
8
+ import subprocess
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+
14
+ def _load_build_version() -> Optional[dict]:
15
+ """Try to load version from build-time generated file.
16
+
17
+ Returns:
18
+ Dictionary with build version info, or None if file doesn't exist.
19
+ """
20
+ try:
21
+ # Import the generated version file
22
+ from . import _version_generated
23
+
24
+ return {
25
+ "version": _version_generated.__version__,
26
+ "date": _version_generated.__build_date__,
27
+ "commit": _version_generated.__commit_id__,
28
+ "package_version": _version_generated.__package_version__,
29
+ }
30
+ except ImportError:
31
+ # Generated file doesn't exist (development mode)
32
+ return None
33
+
34
+
35
+ def _get_git_commit_id() -> Optional[str]:
36
+ """Get the current git commit ID (short form) dynamically.
37
+
38
+ This is used as fallback when build-time version is not available.
39
+
40
+ Returns:
41
+ Short commit ID (first 8 characters) or None if not in a git repo.
42
+ """
43
+ try:
44
+ # Get the directory where this file is located
45
+ repo_dir = Path(__file__).parent.parent.parent
46
+
47
+ result = subprocess.run(
48
+ ["git", "rev-parse", "--short=8", "HEAD"],
49
+ cwd=repo_dir,
50
+ capture_output=True,
51
+ text=True,
52
+ timeout=2,
53
+ )
54
+
55
+ if result.returncode == 0:
56
+ return result.stdout.strip()
57
+ return None
58
+ except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
59
+ return None
60
+
61
+
62
+ def get_version_string() -> str:
63
+ """Get version string in format YYYY.MM.DD.COMMIT_ID.
64
+
65
+ Returns:
66
+ Version string like "2025.11.20.c4a8890" or "2025.11.20.unknown" if no version available.
67
+ """
68
+ # Try build-time version first
69
+ build_version = _load_build_version()
70
+ if build_version:
71
+ return build_version["version"]
72
+
73
+ # Fall back to dynamic git detection (development mode)
74
+ date_str = datetime.now().strftime("%Y.%m.%d")
75
+ commit_id = _get_git_commit_id()
76
+
77
+ if commit_id:
78
+ return f"{date_str}.{commit_id}"
79
+ else:
80
+ return f"{date_str}.unknown"
81
+
82
+
83
+ def get_full_version_info() -> dict:
84
+ """Get detailed version information.
85
+
86
+ Returns:
87
+ Dictionary with version details.
88
+ """
89
+ # Try build-time version first
90
+ build_version = _load_build_version()
91
+ if build_version:
92
+ return build_version
93
+
94
+ # Fall back to dynamic detection (development mode)
95
+ commit_id = _get_git_commit_id()
96
+
97
+ return {
98
+ "version": get_version_string(),
99
+ "date": datetime.now().strftime("%Y.%m.%d"),
100
+ "commit": commit_id or "unknown",
101
+ "package_version": "0.5b0", # From pyproject.toml
102
+ }
103
+
104
+
105
+ if __name__ == "__main__":
106
+ print(get_version_string())
@@ -0,0 +1,8 @@
1
+ """Domain layer - Pure business logic with no infrastructure dependencies."""
2
+
3
+ from .task_lifecycle import TaskLifecycleService, TaskTransitionRule
4
+
5
+ __all__ = [
6
+ "TaskLifecycleService",
7
+ "TaskTransitionRule",
8
+ ]