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.
Files changed (55) hide show
  1. comfy_env/__init__.py +70 -122
  2. comfy_env/cli.py +78 -7
  3. comfy_env/config/__init__.py +19 -0
  4. comfy_env/config/parser.py +151 -0
  5. comfy_env/config/types.py +64 -0
  6. comfy_env/install.py +83 -361
  7. comfy_env/isolation/__init__.py +9 -0
  8. comfy_env/isolation/wrap.py +351 -0
  9. comfy_env/nodes.py +2 -2
  10. comfy_env/pixi/__init__.py +48 -0
  11. comfy_env/pixi/core.py +356 -0
  12. comfy_env/{resolver.py → pixi/resolver.py} +1 -14
  13. comfy_env/prestartup.py +60 -0
  14. comfy_env/templates/comfy-env-instructions.txt +30 -87
  15. comfy_env/templates/comfy-env.toml +68 -136
  16. comfy_env/workers/__init__.py +21 -32
  17. comfy_env/workers/base.py +1 -1
  18. comfy_env/workers/{torch_mp.py → mp.py} +47 -14
  19. comfy_env/workers/{venv.py → subprocess.py} +405 -441
  20. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
  21. comfy_env-0.0.66.dist-info/RECORD +34 -0
  22. comfy_env/decorator.py +0 -700
  23. comfy_env/env/__init__.py +0 -47
  24. comfy_env/env/config.py +0 -201
  25. comfy_env/env/config_file.py +0 -740
  26. comfy_env/env/manager.py +0 -636
  27. comfy_env/env/security.py +0 -267
  28. comfy_env/ipc/__init__.py +0 -55
  29. comfy_env/ipc/bridge.py +0 -476
  30. comfy_env/ipc/protocol.py +0 -265
  31. comfy_env/ipc/tensor.py +0 -371
  32. comfy_env/ipc/torch_bridge.py +0 -401
  33. comfy_env/ipc/transport.py +0 -318
  34. comfy_env/ipc/worker.py +0 -221
  35. comfy_env/isolation.py +0 -310
  36. comfy_env/pixi.py +0 -760
  37. comfy_env/stub_imports.py +0 -270
  38. comfy_env/stubs/__init__.py +0 -1
  39. comfy_env/stubs/comfy/__init__.py +0 -6
  40. comfy_env/stubs/comfy/model_management.py +0 -58
  41. comfy_env/stubs/comfy/utils.py +0 -29
  42. comfy_env/stubs/folder_paths.py +0 -71
  43. comfy_env/workers/pool.py +0 -241
  44. comfy_env-0.0.64.dist-info/RECORD +0 -48
  45. /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
  46. /comfy_env/{env → pixi}/platform/__init__.py +0 -0
  47. /comfy_env/{env → pixi}/platform/base.py +0 -0
  48. /comfy_env/{env → pixi}/platform/darwin.py +0 -0
  49. /comfy_env/{env → pixi}/platform/linux.py +0 -0
  50. /comfy_env/{env → pixi}/platform/windows.py +0 -0
  51. /comfy_env/{registry.py → pixi/registry.py} +0 -0
  52. /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
  53. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
  54. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
  55. {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