comfy-env 0.0.64__py3-none-any.whl → 0.0.66__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.
- comfy_env/__init__.py +70 -122
- comfy_env/cli.py +78 -7
- comfy_env/config/__init__.py +19 -0
- comfy_env/config/parser.py +151 -0
- comfy_env/config/types.py +64 -0
- comfy_env/install.py +83 -361
- comfy_env/isolation/__init__.py +9 -0
- comfy_env/isolation/wrap.py +351 -0
- comfy_env/nodes.py +2 -2
- comfy_env/pixi/__init__.py +48 -0
- comfy_env/pixi/core.py +356 -0
- comfy_env/{resolver.py → pixi/resolver.py} +1 -14
- comfy_env/prestartup.py +60 -0
- comfy_env/templates/comfy-env-instructions.txt +30 -87
- comfy_env/templates/comfy-env.toml +68 -136
- comfy_env/workers/__init__.py +21 -32
- comfy_env/workers/base.py +1 -1
- comfy_env/workers/{torch_mp.py → mp.py} +47 -14
- comfy_env/workers/{venv.py → subprocess.py} +405 -441
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
- comfy_env-0.0.66.dist-info/RECORD +34 -0
- comfy_env/decorator.py +0 -700
- comfy_env/env/__init__.py +0 -47
- comfy_env/env/config.py +0 -201
- comfy_env/env/config_file.py +0 -740
- comfy_env/env/manager.py +0 -636
- comfy_env/env/security.py +0 -267
- comfy_env/ipc/__init__.py +0 -55
- comfy_env/ipc/bridge.py +0 -476
- comfy_env/ipc/protocol.py +0 -265
- comfy_env/ipc/tensor.py +0 -371
- comfy_env/ipc/torch_bridge.py +0 -401
- comfy_env/ipc/transport.py +0 -318
- comfy_env/ipc/worker.py +0 -221
- comfy_env/isolation.py +0 -310
- comfy_env/pixi.py +0 -760
- comfy_env/stub_imports.py +0 -270
- comfy_env/stubs/__init__.py +0 -1
- comfy_env/stubs/comfy/__init__.py +0 -6
- comfy_env/stubs/comfy/model_management.py +0 -58
- comfy_env/stubs/comfy/utils.py +0 -29
- comfy_env/stubs/folder_paths.py +0 -71
- comfy_env/workers/pool.py +0 -241
- comfy_env-0.0.64.dist-info/RECORD +0 -48
- /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
- /comfy_env/{env → pixi}/platform/__init__.py +0 -0
- /comfy_env/{env → pixi}/platform/base.py +0 -0
- /comfy_env/{env → pixi}/platform/darwin.py +0 -0
- /comfy_env/{env → pixi}/platform/linux.py +0 -0
- /comfy_env/{env → pixi}/platform/windows.py +0 -0
- /comfy_env/{registry.py → pixi/registry.py} +0 -0
- /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/licenses/LICENSE +0 -0
comfy_env/ipc/bridge.py
DELETED
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
WorkerBridge - Main IPC class for communicating with isolated workers.
|
|
3
|
-
|
|
4
|
-
This is the primary interface that ComfyUI node developers use.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import subprocess
|
|
9
|
-
import threading
|
|
10
|
-
import uuid
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Any, Callable, Dict, Optional
|
|
13
|
-
|
|
14
|
-
from ..env.config import IsolatedEnv
|
|
15
|
-
from ..env.manager import IsolatedEnvManager
|
|
16
|
-
from .protocol import encode_object, decode_object
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class WorkerBridge:
|
|
20
|
-
"""
|
|
21
|
-
Bridge for communicating with a worker process in an isolated environment.
|
|
22
|
-
|
|
23
|
-
This class manages the worker process lifecycle and handles IPC.
|
|
24
|
-
|
|
25
|
-
Features:
|
|
26
|
-
- Lazy worker startup (starts on first call)
|
|
27
|
-
- Singleton pattern (one worker per environment)
|
|
28
|
-
- Auto-restart on crash
|
|
29
|
-
- Graceful shutdown
|
|
30
|
-
- Timeout support
|
|
31
|
-
|
|
32
|
-
Example:
|
|
33
|
-
from comfy_env import IsolatedEnv, WorkerBridge
|
|
34
|
-
|
|
35
|
-
env = IsolatedEnv(
|
|
36
|
-
name="my-node",
|
|
37
|
-
python="3.10",
|
|
38
|
-
cuda="12.8",
|
|
39
|
-
requirements=["torch==2.8.0"],
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
bridge = WorkerBridge(env, worker_script=Path("worker.py"))
|
|
43
|
-
|
|
44
|
-
# Direct method calls (preferred)
|
|
45
|
-
result = bridge.process(image=my_image)
|
|
46
|
-
|
|
47
|
-
# Or use explicit call() method
|
|
48
|
-
result = bridge.call("process", image=my_image)
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
def __init__(
|
|
52
|
-
self,
|
|
53
|
-
env: IsolatedEnv,
|
|
54
|
-
worker_script: Path,
|
|
55
|
-
base_dir: Optional[Path] = None,
|
|
56
|
-
log_callback: Optional[Callable[[str], None]] = None,
|
|
57
|
-
auto_start: bool = True,
|
|
58
|
-
):
|
|
59
|
-
"""
|
|
60
|
-
Initialize the bridge.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
env: Isolated environment configuration
|
|
64
|
-
worker_script: Path to the worker Python script
|
|
65
|
-
base_dir: Base directory for environments (default: worker_script's parent)
|
|
66
|
-
log_callback: Optional callback for logging (default: print)
|
|
67
|
-
auto_start: Whether to auto-start worker on first call (default: True)
|
|
68
|
-
"""
|
|
69
|
-
self.env = env
|
|
70
|
-
self.worker_script = Path(worker_script)
|
|
71
|
-
self.base_dir = base_dir or self.worker_script.parent
|
|
72
|
-
self.log = log_callback or print
|
|
73
|
-
self.auto_start = auto_start
|
|
74
|
-
|
|
75
|
-
self._manager = IsolatedEnvManager(self.base_dir, log_callback=log_callback)
|
|
76
|
-
self._process: Optional[subprocess.Popen] = None
|
|
77
|
-
self._process_lock = threading.Lock()
|
|
78
|
-
self._stderr_thread: Optional[threading.Thread] = None
|
|
79
|
-
|
|
80
|
-
@classmethod
|
|
81
|
-
def from_config_file(
|
|
82
|
-
cls,
|
|
83
|
-
node_dir: Path,
|
|
84
|
-
worker_script: Optional[Path] = None,
|
|
85
|
-
config_file: Optional[str] = None,
|
|
86
|
-
log_callback: Optional[Callable[[str], None]] = None,
|
|
87
|
-
auto_start: bool = True,
|
|
88
|
-
) -> "WorkerBridge":
|
|
89
|
-
"""
|
|
90
|
-
Create WorkerBridge from a config file.
|
|
91
|
-
|
|
92
|
-
This is a convenience method for loading environment configuration
|
|
93
|
-
from a TOML file instead of defining it programmatically.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
node_dir: Directory containing the config file (and base_dir for env)
|
|
97
|
-
worker_script: Path to worker script (optional - auto-discovered from config or convention)
|
|
98
|
-
config_file: Specific config file name (default: auto-discover)
|
|
99
|
-
log_callback: Optional callback for logging
|
|
100
|
-
auto_start: Whether to auto-start worker on first call (default: True)
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
Configured WorkerBridge instance
|
|
104
|
-
|
|
105
|
-
Raises:
|
|
106
|
-
FileNotFoundError: If no config file found or worker script not found
|
|
107
|
-
ImportError: If tomli not installed (Python < 3.11)
|
|
108
|
-
|
|
109
|
-
Example:
|
|
110
|
-
# Auto-discover everything (recommended)
|
|
111
|
-
bridge = WorkerBridge.from_config_file(node_dir=Path(__file__).parent)
|
|
112
|
-
|
|
113
|
-
# Explicit worker script (backward compatible)
|
|
114
|
-
bridge = WorkerBridge.from_config_file(
|
|
115
|
-
node_dir=Path(__file__).parent,
|
|
116
|
-
worker_script=Path(__file__).parent / "worker.py",
|
|
117
|
-
)
|
|
118
|
-
"""
|
|
119
|
-
from ..env.config_file import load_env_from_file, discover_env_config, CONFIG_FILE_NAMES
|
|
120
|
-
|
|
121
|
-
node_dir = Path(node_dir)
|
|
122
|
-
|
|
123
|
-
if config_file:
|
|
124
|
-
env = load_env_from_file(node_dir / config_file, node_dir)
|
|
125
|
-
else:
|
|
126
|
-
env = discover_env_config(node_dir)
|
|
127
|
-
if env is None:
|
|
128
|
-
raise FileNotFoundError(
|
|
129
|
-
f"No isolation config found in {node_dir}. "
|
|
130
|
-
f"Create one of: {', '.join(CONFIG_FILE_NAMES)}"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# Resolve worker script from explicit param, config, or convention
|
|
134
|
-
if worker_script is None:
|
|
135
|
-
worker_script = cls._resolve_worker_script(node_dir, env)
|
|
136
|
-
|
|
137
|
-
return cls(
|
|
138
|
-
env=env,
|
|
139
|
-
worker_script=worker_script,
|
|
140
|
-
base_dir=node_dir,
|
|
141
|
-
log_callback=log_callback,
|
|
142
|
-
auto_start=auto_start,
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
@staticmethod
|
|
146
|
-
def _resolve_worker_script(node_dir: Path, env: "IsolatedEnv") -> Path:
|
|
147
|
-
"""
|
|
148
|
-
Resolve worker script from config or convention.
|
|
149
|
-
|
|
150
|
-
Discovery order:
|
|
151
|
-
1. Explicit package in config ([worker] package = "worker")
|
|
152
|
-
2. Explicit script in config ([worker] script = "worker.py")
|
|
153
|
-
3. Convention: worker/ directory with __main__.py
|
|
154
|
-
4. Convention: worker.py file
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
node_dir: Node directory
|
|
158
|
-
env: IsolatedEnv with optional worker_package/worker_script
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
Path to worker script
|
|
162
|
-
|
|
163
|
-
Raises:
|
|
164
|
-
FileNotFoundError: If no worker found
|
|
165
|
-
"""
|
|
166
|
-
# 1. Explicit package in config
|
|
167
|
-
if env.worker_package:
|
|
168
|
-
pkg_dir = node_dir / env.worker_package
|
|
169
|
-
main_py = pkg_dir / "__main__.py"
|
|
170
|
-
if main_py.exists():
|
|
171
|
-
return main_py
|
|
172
|
-
raise FileNotFoundError(
|
|
173
|
-
f"Worker package '{env.worker_package}' not found. "
|
|
174
|
-
f"Expected: {main_py}"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
# 2. Explicit script in config
|
|
178
|
-
if env.worker_script:
|
|
179
|
-
script = node_dir / env.worker_script
|
|
180
|
-
if script.exists():
|
|
181
|
-
return script
|
|
182
|
-
raise FileNotFoundError(
|
|
183
|
-
f"Worker script '{env.worker_script}' not found. "
|
|
184
|
-
f"Expected: {script}"
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
# 3. Convention: worker/ directory
|
|
188
|
-
worker_pkg = node_dir / "worker" / "__main__.py"
|
|
189
|
-
if worker_pkg.exists():
|
|
190
|
-
return worker_pkg
|
|
191
|
-
|
|
192
|
-
# 4. Convention: worker.py file
|
|
193
|
-
worker_file = node_dir / "worker.py"
|
|
194
|
-
if worker_file.exists():
|
|
195
|
-
return worker_file
|
|
196
|
-
|
|
197
|
-
raise FileNotFoundError(
|
|
198
|
-
f"No worker found in {node_dir}. "
|
|
199
|
-
f"Create worker/__main__.py, worker.py, or specify in config:\n"
|
|
200
|
-
f" [worker]\n"
|
|
201
|
-
f" package = \"worker\" # or\n"
|
|
202
|
-
f" script = \"worker.py\""
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
@property
|
|
206
|
-
def python_exe(self) -> Path:
|
|
207
|
-
"""Get the Python executable path for the isolated environment."""
|
|
208
|
-
return self._manager.get_python(self.env)
|
|
209
|
-
|
|
210
|
-
@property
|
|
211
|
-
def is_running(self) -> bool:
|
|
212
|
-
"""Check if worker process is currently running."""
|
|
213
|
-
with self._process_lock:
|
|
214
|
-
return self._process is not None and self._process.poll() is None
|
|
215
|
-
|
|
216
|
-
def ensure_environment(self, verify_packages: Optional[list] = None) -> None:
|
|
217
|
-
"""
|
|
218
|
-
Ensure the isolated environment exists and is ready.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
verify_packages: Optional list of packages to verify
|
|
222
|
-
"""
|
|
223
|
-
self._manager.setup(self.env, verify_packages=verify_packages)
|
|
224
|
-
|
|
225
|
-
def start(self) -> None:
|
|
226
|
-
"""
|
|
227
|
-
Start the worker process.
|
|
228
|
-
|
|
229
|
-
Does nothing if worker is already running.
|
|
230
|
-
"""
|
|
231
|
-
with self._process_lock:
|
|
232
|
-
if self._process is not None and self._process.poll() is None:
|
|
233
|
-
return # Already running
|
|
234
|
-
|
|
235
|
-
python_exe = self.python_exe
|
|
236
|
-
if not python_exe.exists():
|
|
237
|
-
raise RuntimeError(
|
|
238
|
-
f"Python executable not found: {python_exe}\n"
|
|
239
|
-
f"Run ensure_environment() first or check your env configuration."
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
if not self.worker_script.exists():
|
|
243
|
-
raise RuntimeError(f"Worker script not found: {self.worker_script}")
|
|
244
|
-
|
|
245
|
-
self.log(f"Starting worker process...")
|
|
246
|
-
self.log(f" Python: {python_exe}")
|
|
247
|
-
self.log(f" Script: {self.worker_script}")
|
|
248
|
-
|
|
249
|
-
self._process = subprocess.Popen(
|
|
250
|
-
[str(python_exe), str(self.worker_script)],
|
|
251
|
-
stdin=subprocess.PIPE,
|
|
252
|
-
stdout=subprocess.PIPE,
|
|
253
|
-
stderr=subprocess.PIPE,
|
|
254
|
-
text=True,
|
|
255
|
-
bufsize=1, # Line buffered
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
# Collect stderr for error reporting
|
|
259
|
-
self._stderr_lines = []
|
|
260
|
-
|
|
261
|
-
# Start stderr reader thread
|
|
262
|
-
self._stderr_thread = threading.Thread(
|
|
263
|
-
target=self._read_stderr,
|
|
264
|
-
daemon=True,
|
|
265
|
-
name=f"worker-stderr-{self.env.name}",
|
|
266
|
-
)
|
|
267
|
-
self._stderr_thread.start()
|
|
268
|
-
|
|
269
|
-
# Give the process a moment to crash if it's going to (e.g., import error)
|
|
270
|
-
import time
|
|
271
|
-
time.sleep(0.5)
|
|
272
|
-
|
|
273
|
-
# Check if process died immediately
|
|
274
|
-
if self._process.poll() is not None:
|
|
275
|
-
exit_code = self._process.returncode
|
|
276
|
-
time.sleep(0.2) # Let stderr thread collect output
|
|
277
|
-
stderr_output = "\n".join(self._stderr_lines[-20:]) # Last 20 lines
|
|
278
|
-
raise RuntimeError(
|
|
279
|
-
f"Worker crashed on startup (exit code {exit_code}).\n"
|
|
280
|
-
f"Stderr output:\n{stderr_output}"
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
# Test connection
|
|
284
|
-
try:
|
|
285
|
-
response = self._send_raw({"method": "ping"}, timeout=30.0)
|
|
286
|
-
if response.get("result") != "pong":
|
|
287
|
-
raise RuntimeError(f"Worker ping failed: {response}")
|
|
288
|
-
self.log("Worker started successfully")
|
|
289
|
-
except Exception as e:
|
|
290
|
-
# Collect any stderr output for debugging
|
|
291
|
-
time.sleep(0.2)
|
|
292
|
-
stderr_output = "\n".join(self._stderr_lines[-20:])
|
|
293
|
-
self.stop()
|
|
294
|
-
raise RuntimeError(f"Worker failed to start: {e}\nStderr:\n{stderr_output}")
|
|
295
|
-
|
|
296
|
-
def _read_stderr(self) -> None:
|
|
297
|
-
"""Read stderr from worker and forward to log callback."""
|
|
298
|
-
if not self._process or not self._process.stderr:
|
|
299
|
-
return
|
|
300
|
-
|
|
301
|
-
for line in self._process.stderr:
|
|
302
|
-
line = line.rstrip()
|
|
303
|
-
if line:
|
|
304
|
-
self.log(line)
|
|
305
|
-
# Also collect for error reporting
|
|
306
|
-
if hasattr(self, '_stderr_lines'):
|
|
307
|
-
self._stderr_lines.append(line)
|
|
308
|
-
|
|
309
|
-
def stop(self) -> None:
|
|
310
|
-
"""
|
|
311
|
-
Stop the worker process gracefully.
|
|
312
|
-
"""
|
|
313
|
-
with self._process_lock:
|
|
314
|
-
if self._process is None or self._process.poll() is not None:
|
|
315
|
-
return
|
|
316
|
-
|
|
317
|
-
self.log("Stopping worker...")
|
|
318
|
-
|
|
319
|
-
# Send shutdown command
|
|
320
|
-
try:
|
|
321
|
-
self._send_raw({"method": "shutdown"}, timeout=5.0)
|
|
322
|
-
except Exception:
|
|
323
|
-
pass
|
|
324
|
-
|
|
325
|
-
# Wait for graceful shutdown
|
|
326
|
-
try:
|
|
327
|
-
self._process.wait(timeout=5.0)
|
|
328
|
-
except subprocess.TimeoutExpired:
|
|
329
|
-
self.log("Worker didn't stop gracefully, terminating...")
|
|
330
|
-
self._process.terminate()
|
|
331
|
-
try:
|
|
332
|
-
self._process.wait(timeout=2.0)
|
|
333
|
-
except subprocess.TimeoutExpired:
|
|
334
|
-
self._process.kill()
|
|
335
|
-
|
|
336
|
-
self._process = None
|
|
337
|
-
self.log("Worker stopped")
|
|
338
|
-
|
|
339
|
-
def _send_raw(self, request: dict, timeout: float = 300.0) -> dict:
|
|
340
|
-
"""
|
|
341
|
-
Send a raw request and wait for response.
|
|
342
|
-
|
|
343
|
-
Args:
|
|
344
|
-
request: Request dict
|
|
345
|
-
timeout: Timeout in seconds
|
|
346
|
-
|
|
347
|
-
Returns:
|
|
348
|
-
Response dict
|
|
349
|
-
"""
|
|
350
|
-
if self._process is None or self._process.poll() is not None:
|
|
351
|
-
raise RuntimeError("Worker process is not running")
|
|
352
|
-
|
|
353
|
-
# Add request ID
|
|
354
|
-
if "id" not in request:
|
|
355
|
-
request["id"] = str(uuid.uuid4())[:8]
|
|
356
|
-
|
|
357
|
-
# Send request
|
|
358
|
-
request_json = json.dumps(request) + "\n"
|
|
359
|
-
self._process.stdin.write(request_json)
|
|
360
|
-
self._process.stdin.flush()
|
|
361
|
-
|
|
362
|
-
# Read response
|
|
363
|
-
import time
|
|
364
|
-
start_time = time.time()
|
|
365
|
-
|
|
366
|
-
while True:
|
|
367
|
-
if time.time() - start_time > timeout:
|
|
368
|
-
raise TimeoutError(f"Timeout waiting for response after {timeout}s")
|
|
369
|
-
|
|
370
|
-
response_line = self._process.stdout.readline()
|
|
371
|
-
if not response_line:
|
|
372
|
-
raise RuntimeError("Worker process closed unexpectedly")
|
|
373
|
-
|
|
374
|
-
response_line = response_line.strip()
|
|
375
|
-
if not response_line:
|
|
376
|
-
continue
|
|
377
|
-
|
|
378
|
-
# Skip non-JSON lines (library output that escaped)
|
|
379
|
-
if response_line.startswith('[') and not response_line.startswith('[{'):
|
|
380
|
-
continue
|
|
381
|
-
|
|
382
|
-
try:
|
|
383
|
-
return json.loads(response_line)
|
|
384
|
-
except json.JSONDecodeError:
|
|
385
|
-
continue
|
|
386
|
-
|
|
387
|
-
def call(
|
|
388
|
-
self,
|
|
389
|
-
method: str,
|
|
390
|
-
timeout: float = 300.0,
|
|
391
|
-
**kwargs,
|
|
392
|
-
) -> Any:
|
|
393
|
-
"""
|
|
394
|
-
Call a method on the worker.
|
|
395
|
-
|
|
396
|
-
Args:
|
|
397
|
-
method: Method name to call
|
|
398
|
-
timeout: Timeout in seconds (default: 5 minutes)
|
|
399
|
-
**kwargs: Arguments to pass to the method
|
|
400
|
-
|
|
401
|
-
Returns:
|
|
402
|
-
The method's return value
|
|
403
|
-
|
|
404
|
-
Raises:
|
|
405
|
-
RuntimeError: If worker returns an error
|
|
406
|
-
TimeoutError: If call times out
|
|
407
|
-
"""
|
|
408
|
-
# Auto-start if needed
|
|
409
|
-
if self.auto_start and not self.is_running:
|
|
410
|
-
self.start()
|
|
411
|
-
|
|
412
|
-
# Encode arguments
|
|
413
|
-
encoded_args = encode_object(kwargs)
|
|
414
|
-
|
|
415
|
-
# Send request
|
|
416
|
-
request = {
|
|
417
|
-
"method": method,
|
|
418
|
-
"args": encoded_args,
|
|
419
|
-
}
|
|
420
|
-
response = self._send_raw(request, timeout=timeout)
|
|
421
|
-
|
|
422
|
-
# Check for error
|
|
423
|
-
if "error" in response and response["error"]:
|
|
424
|
-
error_msg = response["error"]
|
|
425
|
-
tb = response.get("traceback", "")
|
|
426
|
-
raise RuntimeError(f"Worker error: {error_msg}\n{tb}")
|
|
427
|
-
|
|
428
|
-
# Decode and return result
|
|
429
|
-
return decode_object(response.get("result"))
|
|
430
|
-
|
|
431
|
-
def __enter__(self) -> "WorkerBridge":
|
|
432
|
-
"""Context manager entry."""
|
|
433
|
-
return self
|
|
434
|
-
|
|
435
|
-
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
436
|
-
"""Context manager exit - stops worker."""
|
|
437
|
-
self.stop()
|
|
438
|
-
|
|
439
|
-
def __del__(self) -> None:
|
|
440
|
-
"""Destructor - stops worker."""
|
|
441
|
-
try:
|
|
442
|
-
self.stop()
|
|
443
|
-
except Exception:
|
|
444
|
-
pass
|
|
445
|
-
|
|
446
|
-
def __getattr__(self, name: str) -> Callable[..., Any]:
|
|
447
|
-
"""
|
|
448
|
-
Enable direct method calls on the bridge.
|
|
449
|
-
|
|
450
|
-
Instead of:
|
|
451
|
-
result = bridge.call("inference", image=img)
|
|
452
|
-
|
|
453
|
-
You can do:
|
|
454
|
-
result = bridge.inference(image=img)
|
|
455
|
-
|
|
456
|
-
Args:
|
|
457
|
-
name: Method name to call on the worker
|
|
458
|
-
|
|
459
|
-
Returns:
|
|
460
|
-
A callable that forwards to self.call()
|
|
461
|
-
"""
|
|
462
|
-
# Don't intercept private/magic attributes
|
|
463
|
-
if name.startswith("_"):
|
|
464
|
-
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
465
|
-
|
|
466
|
-
# Return a wrapper that calls the method on the worker
|
|
467
|
-
def method_wrapper(*args, timeout: float = 300.0, **kwargs) -> Any:
|
|
468
|
-
# If positional args provided, raise helpful error
|
|
469
|
-
if args:
|
|
470
|
-
raise TypeError(
|
|
471
|
-
f"bridge.{name}() doesn't accept positional arguments. "
|
|
472
|
-
f"Use keyword arguments: bridge.{name}(arg1=val1, arg2=val2)"
|
|
473
|
-
)
|
|
474
|
-
return self.call(name, timeout=timeout, **kwargs)
|
|
475
|
-
|
|
476
|
-
return method_wrapper
|