capiscio-sdk 0.3.0__py3-none-any.whl → 2.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ # Generated protobuf modules
@@ -0,0 +1,232 @@
1
+ """Process manager for the capiscio-core gRPC server."""
2
+
3
+ import atexit
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ # Default socket path
12
+ DEFAULT_SOCKET_DIR = Path.home() / ".capiscio"
13
+ DEFAULT_SOCKET_PATH = DEFAULT_SOCKET_DIR / "rpc.sock"
14
+
15
+
16
+ class ProcessManager:
17
+ """Manages the capiscio-core gRPC server process.
18
+
19
+ This class handles:
20
+ - Finding the capiscio binary
21
+ - Starting the gRPC server process
22
+ - Managing the process lifecycle
23
+ - Cleanup on exit
24
+
25
+ Usage:
26
+ manager = ProcessManager()
27
+ manager.ensure_running()
28
+ # ... use gRPC client ...
29
+ manager.stop() # Optional, will auto-stop on exit
30
+ """
31
+
32
+ _instance: Optional["ProcessManager"] = None
33
+ _process: Optional[subprocess.Popen] = None
34
+ _socket_path: Optional[Path] = None
35
+ _tcp_address: Optional[str] = None
36
+
37
+ def __new__(cls) -> "ProcessManager":
38
+ """Singleton pattern - only one process manager per Python process."""
39
+ if cls._instance is None:
40
+ cls._instance = super().__new__(cls)
41
+ cls._instance._initialized = False
42
+ return cls._instance
43
+
44
+ def __init__(self) -> None:
45
+ if hasattr(self, '_initialized') and self._initialized:
46
+ return
47
+ self._initialized = True
48
+ self._binary_path: Optional[Path] = None
49
+ self._started = False
50
+
51
+ # Register cleanup on exit
52
+ atexit.register(self.stop)
53
+
54
+ @property
55
+ def address(self) -> str:
56
+ """Get the address to connect to (unix socket or tcp)."""
57
+ if self._tcp_address:
58
+ return self._tcp_address
59
+ if self._socket_path:
60
+ return f"unix://{self._socket_path}"
61
+ return f"unix://{DEFAULT_SOCKET_PATH}"
62
+
63
+ @property
64
+ def is_running(self) -> bool:
65
+ """Check if the server process is running."""
66
+ if self._process is None:
67
+ return False
68
+ return self._process.poll() is None
69
+
70
+ def find_binary(self) -> Optional[Path]:
71
+ """Find the capiscio binary.
72
+
73
+ Search order:
74
+ 1. CAPISCIO_BINARY environment variable
75
+ 2. capiscio-core/bin/capiscio relative to SDK
76
+ 3. System PATH
77
+ """
78
+ # Check environment variable
79
+ env_path = os.environ.get("CAPISCIO_BINARY")
80
+ if env_path:
81
+ path = Path(env_path)
82
+ if path.exists() and path.is_file():
83
+ return path
84
+
85
+ # Check relative to this file (development mode)
86
+ # SDK is at capiscio-sdk-python/capiscio_sdk/_rpc/
87
+ # Binary is at capiscio-core/bin/capiscio
88
+ sdk_root = Path(__file__).parent.parent.parent
89
+ workspace_root = sdk_root.parent
90
+ dev_binary = workspace_root / "capiscio-core" / "bin" / "capiscio"
91
+ if dev_binary.exists():
92
+ return dev_binary
93
+
94
+ # Check system PATH
95
+ which_result = shutil.which("capiscio")
96
+ if which_result:
97
+ return Path(which_result)
98
+
99
+ return None
100
+
101
+ def ensure_running(
102
+ self,
103
+ socket_path: Optional[Path] = None,
104
+ tcp_address: Optional[str] = None,
105
+ timeout: float = 5.0,
106
+ ) -> str:
107
+ """Ensure the gRPC server is running.
108
+
109
+ Args:
110
+ socket_path: Path for Unix socket (default: ~/.capiscio/rpc.sock)
111
+ tcp_address: TCP address to use instead of socket (e.g., "localhost:50051")
112
+ timeout: Seconds to wait for server to start
113
+
114
+ Returns:
115
+ The address to connect to
116
+
117
+ Raises:
118
+ RuntimeError: If binary not found or server fails to start
119
+ """
120
+ # If using external server (TCP), just return the address
121
+ if tcp_address:
122
+ self._tcp_address = tcp_address
123
+ return tcp_address
124
+
125
+ # Check if already running
126
+ if self.is_running:
127
+ return self.address
128
+
129
+ # Find binary
130
+ binary = self.find_binary()
131
+ if binary is None:
132
+ raise RuntimeError(
133
+ "capiscio binary not found. Please either:\n"
134
+ " 1. Set CAPISCIO_BINARY environment variable\n"
135
+ " 2. Install capiscio-core and add to PATH\n"
136
+ " 3. Build capiscio-core locally"
137
+ )
138
+ self._binary_path = binary
139
+
140
+ # Set up socket path
141
+ self._socket_path = socket_path or DEFAULT_SOCKET_PATH
142
+
143
+ # Ensure socket directory exists
144
+ self._socket_path.parent.mkdir(parents=True, exist_ok=True)
145
+
146
+ # Remove stale socket
147
+ if self._socket_path.exists():
148
+ self._socket_path.unlink()
149
+
150
+ # Start the server
151
+ cmd = [str(binary), "rpc", "--socket", str(self._socket_path)]
152
+
153
+ try:
154
+ self._process = subprocess.Popen(
155
+ cmd,
156
+ stdout=subprocess.PIPE,
157
+ stderr=subprocess.PIPE,
158
+ start_new_session=True, # Don't forward signals
159
+ )
160
+ except Exception as e:
161
+ raise RuntimeError(f"Failed to start capiscio server: {e}") from e
162
+
163
+ # Wait for socket to appear
164
+ start_time = time.time()
165
+ while time.time() - start_time < timeout:
166
+ if self._socket_path.exists():
167
+ self._started = True
168
+ return self.address
169
+
170
+ # Check if process died
171
+ if self._process.poll() is not None:
172
+ stdout, stderr = self._process.communicate()
173
+ raise RuntimeError(
174
+ f"capiscio server exited unexpectedly:\n"
175
+ f"stdout: {stdout.decode()}\n"
176
+ f"stderr: {stderr.decode()}"
177
+ )
178
+
179
+ time.sleep(0.1)
180
+
181
+ # Timeout - kill process and raise
182
+ self.stop()
183
+ raise RuntimeError(
184
+ f"capiscio server did not start within {timeout}s. "
185
+ f"Socket not found at {self._socket_path}"
186
+ )
187
+
188
+ def stop(self) -> None:
189
+ """Stop the gRPC server process."""
190
+ if self._process is None:
191
+ return
192
+
193
+ if self._process.poll() is None:
194
+ # Process still running, terminate gracefully
195
+ try:
196
+ self._process.terminate()
197
+ self._process.wait(timeout=5.0)
198
+ except subprocess.TimeoutExpired:
199
+ # Force kill
200
+ self._process.kill()
201
+ self._process.wait()
202
+
203
+ self._process = None
204
+ self._started = False
205
+
206
+ # Clean up socket
207
+ if self._socket_path and self._socket_path.exists():
208
+ try:
209
+ self._socket_path.unlink()
210
+ except OSError:
211
+ # Socket may have been cleaned up by another process
212
+ pass
213
+
214
+ def restart(self) -> str:
215
+ """Restart the gRPC server."""
216
+ self.stop()
217
+ return self.ensure_running(
218
+ socket_path=self._socket_path,
219
+ tcp_address=self._tcp_address,
220
+ )
221
+
222
+
223
+ # Global instance for convenience
224
+ _manager: Optional[ProcessManager] = None
225
+
226
+
227
+ def get_process_manager() -> ProcessManager:
228
+ """Get the global ProcessManager instance."""
229
+ global _manager
230
+ if _manager is None:
231
+ _manager = ProcessManager()
232
+ return _manager