capiscio-sdk 0.2.0__py3-none-any.whl → 2.3.0__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.
- capiscio_sdk/__init__.py +69 -1
- capiscio_sdk/_rpc/__init__.py +7 -0
- capiscio_sdk/_rpc/client.py +1321 -0
- capiscio_sdk/_rpc/gen/__init__.py +1 -0
- capiscio_sdk/_rpc/process.py +232 -0
- capiscio_sdk/badge.py +737 -0
- capiscio_sdk/badge_keeper.py +304 -0
- capiscio_sdk/config.py +1 -1
- capiscio_sdk/dv.py +296 -0
- capiscio_sdk/errors.py +11 -1
- capiscio_sdk/executor.py +17 -0
- capiscio_sdk/integrations/fastapi.py +74 -0
- capiscio_sdk/scoring/__init__.py +73 -3
- capiscio_sdk/simple_guard.py +346 -0
- capiscio_sdk/types.py +1 -1
- capiscio_sdk/validators/__init__.py +59 -2
- capiscio_sdk/validators/_core.py +376 -0
- capiscio_sdk-2.3.0.dist-info/METADATA +532 -0
- capiscio_sdk-2.3.0.dist-info/RECORD +36 -0
- {capiscio_sdk-0.2.0.dist-info → capiscio_sdk-2.3.0.dist-info}/WHEEL +1 -1
- capiscio_sdk-0.2.0.dist-info/METADATA +0 -221
- capiscio_sdk-0.2.0.dist-info/RECORD +0 -26
- {capiscio_sdk-0.2.0.dist-info → capiscio_sdk-2.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|