comfygit-core 0.2.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.
Files changed (93) hide show
  1. comfygit_core/analyzers/custom_node_scanner.py +109 -0
  2. comfygit_core/analyzers/git_change_parser.py +156 -0
  3. comfygit_core/analyzers/model_scanner.py +318 -0
  4. comfygit_core/analyzers/node_classifier.py +58 -0
  5. comfygit_core/analyzers/node_git_analyzer.py +77 -0
  6. comfygit_core/analyzers/status_scanner.py +362 -0
  7. comfygit_core/analyzers/workflow_dependency_parser.py +143 -0
  8. comfygit_core/caching/__init__.py +16 -0
  9. comfygit_core/caching/api_cache.py +210 -0
  10. comfygit_core/caching/base.py +212 -0
  11. comfygit_core/caching/comfyui_cache.py +100 -0
  12. comfygit_core/caching/custom_node_cache.py +320 -0
  13. comfygit_core/caching/workflow_cache.py +797 -0
  14. comfygit_core/clients/__init__.py +4 -0
  15. comfygit_core/clients/civitai_client.py +412 -0
  16. comfygit_core/clients/github_client.py +349 -0
  17. comfygit_core/clients/registry_client.py +230 -0
  18. comfygit_core/configs/comfyui_builtin_nodes.py +1614 -0
  19. comfygit_core/configs/comfyui_models.py +62 -0
  20. comfygit_core/configs/model_config.py +151 -0
  21. comfygit_core/constants.py +82 -0
  22. comfygit_core/core/environment.py +1635 -0
  23. comfygit_core/core/workspace.py +898 -0
  24. comfygit_core/factories/environment_factory.py +419 -0
  25. comfygit_core/factories/uv_factory.py +61 -0
  26. comfygit_core/factories/workspace_factory.py +109 -0
  27. comfygit_core/infrastructure/sqlite_manager.py +156 -0
  28. comfygit_core/integrations/__init__.py +7 -0
  29. comfygit_core/integrations/uv_command.py +318 -0
  30. comfygit_core/logging/logging_config.py +15 -0
  31. comfygit_core/managers/environment_git_orchestrator.py +316 -0
  32. comfygit_core/managers/environment_model_manager.py +296 -0
  33. comfygit_core/managers/export_import_manager.py +116 -0
  34. comfygit_core/managers/git_manager.py +667 -0
  35. comfygit_core/managers/model_download_manager.py +252 -0
  36. comfygit_core/managers/model_symlink_manager.py +166 -0
  37. comfygit_core/managers/node_manager.py +1378 -0
  38. comfygit_core/managers/pyproject_manager.py +1321 -0
  39. comfygit_core/managers/user_content_symlink_manager.py +436 -0
  40. comfygit_core/managers/uv_project_manager.py +569 -0
  41. comfygit_core/managers/workflow_manager.py +1944 -0
  42. comfygit_core/models/civitai.py +432 -0
  43. comfygit_core/models/commit.py +18 -0
  44. comfygit_core/models/environment.py +293 -0
  45. comfygit_core/models/exceptions.py +378 -0
  46. comfygit_core/models/manifest.py +132 -0
  47. comfygit_core/models/node_mapping.py +201 -0
  48. comfygit_core/models/protocols.py +248 -0
  49. comfygit_core/models/registry.py +63 -0
  50. comfygit_core/models/shared.py +356 -0
  51. comfygit_core/models/sync.py +42 -0
  52. comfygit_core/models/system.py +204 -0
  53. comfygit_core/models/workflow.py +914 -0
  54. comfygit_core/models/workspace_config.py +71 -0
  55. comfygit_core/py.typed +0 -0
  56. comfygit_core/repositories/migrate_paths.py +49 -0
  57. comfygit_core/repositories/model_repository.py +958 -0
  58. comfygit_core/repositories/node_mappings_repository.py +246 -0
  59. comfygit_core/repositories/workflow_repository.py +57 -0
  60. comfygit_core/repositories/workspace_config_repository.py +121 -0
  61. comfygit_core/resolvers/global_node_resolver.py +459 -0
  62. comfygit_core/resolvers/model_resolver.py +250 -0
  63. comfygit_core/services/import_analyzer.py +218 -0
  64. comfygit_core/services/model_downloader.py +422 -0
  65. comfygit_core/services/node_lookup_service.py +251 -0
  66. comfygit_core/services/registry_data_manager.py +161 -0
  67. comfygit_core/strategies/__init__.py +4 -0
  68. comfygit_core/strategies/auto.py +72 -0
  69. comfygit_core/strategies/confirmation.py +69 -0
  70. comfygit_core/utils/comfyui_ops.py +125 -0
  71. comfygit_core/utils/common.py +164 -0
  72. comfygit_core/utils/conflict_parser.py +232 -0
  73. comfygit_core/utils/dependency_parser.py +231 -0
  74. comfygit_core/utils/download.py +216 -0
  75. comfygit_core/utils/environment_cleanup.py +111 -0
  76. comfygit_core/utils/filesystem.py +178 -0
  77. comfygit_core/utils/git.py +1184 -0
  78. comfygit_core/utils/input_signature.py +145 -0
  79. comfygit_core/utils/model_categories.py +52 -0
  80. comfygit_core/utils/pytorch.py +71 -0
  81. comfygit_core/utils/requirements.py +211 -0
  82. comfygit_core/utils/retry.py +242 -0
  83. comfygit_core/utils/symlink_utils.py +119 -0
  84. comfygit_core/utils/system_detector.py +258 -0
  85. comfygit_core/utils/uuid.py +28 -0
  86. comfygit_core/utils/uv_error_handler.py +158 -0
  87. comfygit_core/utils/version.py +73 -0
  88. comfygit_core/utils/workflow_hash.py +90 -0
  89. comfygit_core/validation/resolution_tester.py +297 -0
  90. comfygit_core-0.2.0.dist-info/METADATA +939 -0
  91. comfygit_core-0.2.0.dist-info/RECORD +93 -0
  92. comfygit_core-0.2.0.dist-info/WHEEL +4 -0
  93. comfygit_core-0.2.0.dist-info/licenses/LICENSE.txt +661 -0
@@ -0,0 +1,156 @@
1
+ """Generic SQLite database operations utility."""
2
+
3
+ import sqlite3
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..logging.logging_config import get_logger
10
+ from ..models.exceptions import ComfyDockError
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class SQLiteManager:
16
+ """Generic SQLite database manager with connection management."""
17
+
18
+ def __init__(self, db_path: Path):
19
+ """Initialize SQLite manager.
20
+
21
+ Args:
22
+ db_path: Path to SQLite database file
23
+ """
24
+ self.db_path = db_path
25
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
26
+
27
+ @contextmanager
28
+ def get_connection(self) -> Generator[sqlite3.Connection, None, None]:
29
+ """Get database connection with context management.
30
+
31
+ Yields:
32
+ SQLite connection with row factory enabled
33
+
34
+ Raises:
35
+ ComfyDockError: If database connection fails
36
+ """
37
+ conn = None
38
+ try:
39
+ conn = sqlite3.connect(self.db_path)
40
+ conn.row_factory = sqlite3.Row
41
+ yield conn
42
+ except sqlite3.Error as e:
43
+ logger.error(f"Database error: {e}")
44
+ raise ComfyDockError(f"Database operation failed: {e}")
45
+ finally:
46
+ if conn:
47
+ conn.close()
48
+
49
+ def execute_query(self, query: str, params: tuple = ()) -> list[dict[str, Any]]:
50
+ """Execute SELECT query and return results.
51
+
52
+ Args:
53
+ query: SQL SELECT query
54
+ params: Query parameters
55
+
56
+ Returns:
57
+ List of dictionaries representing rows
58
+
59
+ Raises:
60
+ ComfyDockError: If query execution fails
61
+ """
62
+ with self.get_connection() as conn:
63
+ try:
64
+ cursor = conn.cursor()
65
+ cursor.execute(query, params)
66
+ rows = cursor.fetchall()
67
+ return [dict(row) for row in rows]
68
+ except sqlite3.Error as e:
69
+ logger.error(f"Query execution failed: {query} with params {params}: {e}")
70
+ raise ComfyDockError(f"Query execution failed: {e}")
71
+
72
+ def execute_write(self, query: str, params: tuple = ()) -> int:
73
+ """Execute INSERT/UPDATE/DELETE query.
74
+
75
+ Args:
76
+ query: SQL write query
77
+ params: Query parameters
78
+
79
+ Returns:
80
+ Number of affected rows
81
+
82
+ Raises:
83
+ ComfyDockError: If write operation fails
84
+ """
85
+ with self.get_connection() as conn:
86
+ try:
87
+ cursor = conn.cursor()
88
+ cursor.execute(query, params)
89
+ conn.commit()
90
+ return cursor.rowcount
91
+ except sqlite3.Error as e:
92
+ logger.error(f"Write operation failed: {query} with params {params}: {e}")
93
+ conn.rollback()
94
+ raise ComfyDockError(f"Write operation failed: {e}")
95
+
96
+ def create_table(self, schema: str) -> None:
97
+ """Create table using schema SQL.
98
+
99
+ Args:
100
+ schema: CREATE TABLE SQL statement
101
+
102
+ Raises:
103
+ ComfyDockError: If table creation fails
104
+ """
105
+ with self.get_connection() as conn:
106
+ try:
107
+ cursor = conn.cursor()
108
+ cursor.execute(schema)
109
+ conn.commit()
110
+ logger.debug("Table schema ensured")
111
+ except sqlite3.Error as e:
112
+ logger.error(f"Table creation failed: {schema}: {e}")
113
+ raise ComfyDockError(f"Table creation failed: {e}")
114
+
115
+ def begin_transaction(self) -> sqlite3.Connection:
116
+ """Begin a transaction and return connection for manual management.
117
+
118
+ Returns:
119
+ SQLite connection with transaction started
120
+
121
+ Note:
122
+ Caller is responsible for commit/rollback and closing connection
123
+ """
124
+ try:
125
+ conn = sqlite3.connect(self.db_path)
126
+ conn.row_factory = sqlite3.Row
127
+ conn.execute("BEGIN")
128
+ return conn
129
+ except sqlite3.Error as e:
130
+ logger.error(f"Transaction start failed: {e}")
131
+ raise ComfyDockError(f"Transaction start failed: {e}")
132
+
133
+ def table_exists(self, table_name: str) -> bool:
134
+ """Check if table exists in database.
135
+
136
+ Args:
137
+ table_name: Name of table to check
138
+
139
+ Returns:
140
+ True if table exists, False otherwise
141
+ """
142
+ query = "SELECT name FROM sqlite_master WHERE type='table' AND name=?"
143
+ results = self.execute_query(query, (table_name,))
144
+ return len(results) > 0
145
+
146
+ def get_table_info(self, table_name: str) -> list[dict[str, Any]]:
147
+ """Get table schema information.
148
+
149
+ Args:
150
+ table_name: Name of table
151
+
152
+ Returns:
153
+ List of column information dictionaries
154
+ """
155
+ query = f"PRAGMA table_info({table_name})"
156
+ return self.execute_query(query)
@@ -0,0 +1,7 @@
1
+ """Integration modules for external tools and services."""
2
+
3
+ from .uv_command import UVCommand
4
+
5
+ __all__ = [
6
+ 'UVCommand',
7
+ ]
@@ -0,0 +1,318 @@
1
+ """Pure UV command wrapper with no business logic."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from ..logging.logging_config import get_logger
11
+ from ..models.exceptions import UVCommandError, UVNotInstalledError
12
+ from ..utils.common import run_command
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class CommandResult:
19
+ """Result of a UV command execution."""
20
+ stdout: str
21
+ stderr: str
22
+ returncode: int
23
+ success: bool
24
+
25
+ @classmethod
26
+ def from_completed_process(cls, result) -> "CommandResult":
27
+ return cls(
28
+ stdout=result.stdout,
29
+ stderr=result.stderr,
30
+ returncode=result.returncode,
31
+ success=result.returncode == 0
32
+ )
33
+
34
+
35
+ class UVCommand:
36
+ """Pure wrapper around UV CLI commands. No business logic or pyproject.toml manipulation."""
37
+
38
+ DEFAULT_TIMEOUT = None # no timeout
39
+
40
+ def __init__(
41
+ self,
42
+ binary_path: Path | None = None,
43
+ project_env: Path | None = None,
44
+ cache_dir: Path | None = None,
45
+ python_install_dir: Path | None = None,
46
+ link_mode: str | None = "hardlink",
47
+ cwd: Path | None = None,
48
+ torch_backend: str | None = None,
49
+ ):
50
+ self._binary = self._check_uv_installed(binary_path)
51
+ self.timeout = self.DEFAULT_TIMEOUT
52
+ self._project_env = project_env
53
+ self._cache_dir = cache_dir
54
+ self._python_install_dir = python_install_dir
55
+ self._link_mode = link_mode
56
+ self._cwd = cwd
57
+ self._torch_backend = torch_backend
58
+ self._base_env = self._setup_base_environment()
59
+
60
+ def _check_uv_installed(self, binary_path: Path | None) -> str:
61
+ # Explicit path takes priority
62
+ if binary_path and binary_path.is_file():
63
+ return str(binary_path)
64
+
65
+ # Try UV from Python package (installed with comfydock)
66
+ try:
67
+ from uv import find_uv_bin
68
+ binary = find_uv_bin()
69
+ logger.debug(f"Using UV from package: {binary}")
70
+ return binary
71
+ except ImportError:
72
+ logger.debug("UV package not found in current environment")
73
+ except FileNotFoundError as e:
74
+ logger.warning(f"UV package found but binary missing: {e}")
75
+
76
+ # Fallback to system UV
77
+ binary = shutil.which("uv")
78
+ if binary is None:
79
+ raise UVNotInstalledError(
80
+ "uv is not installed. Install comfydock with: pip install comfygit"
81
+ )
82
+
83
+ logger.warning(
84
+ f"Using system UV from PATH: {binary}. "
85
+ f"This may cause version compatibility issues. "
86
+ f"Recommended: pip install --force-reinstall comfydock-cli"
87
+ )
88
+ return binary
89
+
90
+ def _setup_base_environment(self) -> dict[str, str]:
91
+ env = os.environ.copy()
92
+ env.update({
93
+ "UV_NO_PROGRESS": "1",
94
+ "NO_COLOR": "1",
95
+ "VIRTUAL_ENV": "",
96
+ })
97
+
98
+ if self._project_env:
99
+ env["UV_PROJECT_ENVIRONMENT"] = str(self._project_env)
100
+ if self._cache_dir:
101
+ env["UV_CACHE_DIR"] = str(self._cache_dir)
102
+ if self._python_install_dir:
103
+ env["UV_PYTHON_INSTALL_DIR"] = str(self._python_install_dir)
104
+ if self._link_mode:
105
+ env["UV_LINK_MODE"] = self._link_mode
106
+ if self._torch_backend:
107
+ env["UV_TORCH_BACKEND"] = self._torch_backend
108
+
109
+ return env
110
+
111
+ def _build_command(self, base: list[str], **options) -> list[str]:
112
+ cmd = [self._binary] + base
113
+
114
+ flag_map = {
115
+ 'python': '--python',
116
+ 'index_url': '--index-url',
117
+ 'frozen': '--frozen',
118
+ 'dry_run': '--dry-run',
119
+ 'no_sync': '--no-sync',
120
+ 'name': '--name',
121
+ 'no_workspace': '--no-workspace',
122
+ 'bare': '--bare',
123
+ 'raw': '--raw',
124
+ 'dev': '--dev',
125
+ 'group': '--group',
126
+ 'editable': '--editable',
127
+ 'bounds': '--bounds',
128
+ 'prerelease': '--prerelease',
129
+ 'all_groups': '--all-groups',
130
+ 'no_default_groups': '--no-default-groups',
131
+ 'seed': '--seed',
132
+ 'upgrade': '--upgrade',
133
+ 'no_install_project': '--no-install-project',
134
+ 'no_deps': '--no-deps',
135
+ 'compile_bytecode': '--compile-bytecode',
136
+ 'quiet': '--quiet',
137
+ }
138
+
139
+ for key, value in options.items():
140
+ if value is None or value is False:
141
+ continue
142
+
143
+ flag = flag_map.get(key)
144
+ if flag:
145
+ if isinstance(value, bool):
146
+ cmd.append(flag)
147
+ elif isinstance(value, list):
148
+ # Handle list values that need multiple flags (e.g., --group x --group y)
149
+ for item in value:
150
+ cmd.extend([flag, str(item)])
151
+ else:
152
+ cmd.extend([flag, str(value)])
153
+
154
+ return cmd
155
+
156
+ def _execute(self, cmd: list[str], expect_failure: bool = False, verbose: bool = False) -> CommandResult:
157
+ try:
158
+ env = self._base_env.copy()
159
+ if verbose:
160
+ # Show full output with progress and summary
161
+ env.pop("UV_NO_PROGRESS", None)
162
+ env.pop("NO_COLOR", None)
163
+ result = run_command(cmd, cwd=self._cwd, timeout=self.timeout, env=env, capture_output=False)
164
+ else:
165
+ # Default: quiet mode (capture output, only show on error)
166
+ result = run_command(cmd, cwd=self._cwd, timeout=self.timeout, env=self._base_env, capture_output=True)
167
+
168
+ if result.returncode == 0 or expect_failure:
169
+ return CommandResult.from_completed_process(result)
170
+ else:
171
+ raise UVCommandError(
172
+ f"UV command failed with code {result.returncode}",
173
+ command=cmd,
174
+ stderr=result.stderr,
175
+ stdout=result.stdout,
176
+ returncode=result.returncode
177
+ )
178
+ except subprocess.TimeoutExpired:
179
+ raise TimeoutError(f"UV command timed out after {self.timeout}s: {' '.join(cmd)}")
180
+ except UVCommandError:
181
+ raise
182
+ except Exception as e:
183
+ raise UVCommandError(f"Failed to execute UV command: {e}", command=cmd) from e
184
+
185
+ # ===== Project Management Commands =====
186
+
187
+ def init(self, name: str | None = None, python: str | None = None, **flags) -> CommandResult:
188
+ cmd = self._build_command(["init"], name=name, python=python, **flags)
189
+ return self._execute(cmd)
190
+
191
+ def add(self, packages: list[str] | None = None, requirements_file: Path | None = None, **flags) -> CommandResult:
192
+ if requirements_file:
193
+ cmd = self._build_command(["add", "-r", str(requirements_file)], **flags)
194
+ else:
195
+ cmd = self._build_command(["add"] + (packages or []), **flags)
196
+ return self._execute(cmd)
197
+
198
+ def remove(self, packages: list[str], **flags) -> CommandResult:
199
+ cmd = self._build_command(["remove"] + packages, **flags)
200
+ return self._execute(cmd)
201
+
202
+ def sync(self, verbose: bool = False, **flags) -> CommandResult:
203
+ cmd = self._build_command(["sync"], **flags)
204
+ return self._execute(cmd, verbose=verbose)
205
+
206
+ def lock(self, **flags) -> CommandResult:
207
+ cmd = self._build_command(["lock"], **flags)
208
+ return self._execute(cmd, expect_failure=flags.get('dry_run', False))
209
+
210
+ def run(self, command: list[str], **flags) -> CommandResult:
211
+ cmd = self._build_command(["run"] + command, **flags)
212
+ return self._execute(cmd)
213
+
214
+ # ===== Virtual Environment Management =====
215
+
216
+ def venv(self, path: Path, **flags) -> CommandResult:
217
+ cmd = self._build_command(["venv", str(path)], **flags)
218
+ return self._execute(cmd)
219
+
220
+ # ===== Pip Compatibility =====
221
+
222
+ def pip_install(self, packages: list[str] | None = None, requirements_file: Path | None = None,
223
+ python: Path | None = None, torch_backend: str | None = None,
224
+ verbose: bool = False, **flags) -> CommandResult:
225
+ cmd = [self._binary, "pip", "install"]
226
+
227
+ if python:
228
+ cmd.extend(["--python", str(python)])
229
+
230
+ if torch_backend:
231
+ cmd.extend(["--torch-backend", torch_backend])
232
+
233
+ for key, value in flags.items():
234
+ if value is None or value is False:
235
+ continue
236
+ flag = f"--{key.replace('_', '-')}"
237
+ if isinstance(value, bool):
238
+ cmd.append(flag)
239
+ else:
240
+ cmd.extend([flag, str(value)])
241
+
242
+ if requirements_file:
243
+ cmd.extend(["-r", str(requirements_file)])
244
+ elif packages:
245
+ cmd.extend(packages)
246
+
247
+ return self._execute(cmd, verbose=verbose)
248
+
249
+ def pip_show(self, package: str, python: Path, **flags) -> CommandResult:
250
+ cmd = [self._binary, "pip", "show", "--python", str(python), package]
251
+ return self._execute(cmd)
252
+
253
+ def pip_list(self, python: Path, **flags) -> CommandResult:
254
+ cmd = [self._binary, "pip", "list", "--python", str(python)]
255
+ return self._execute(cmd)
256
+
257
+ def pip_freeze(self, python: Path, **flags) -> CommandResult:
258
+ cmd = [self._binary, "pip", "freeze", "--python", str(python)]
259
+ return self._execute(cmd)
260
+
261
+ def pip_compile(self, input_file: Path | None = None, output_file: Path | None = None, **flags) -> CommandResult:
262
+ cmd = [self._binary, "pip", "compile"]
263
+
264
+ if input_file:
265
+ cmd.append(str(input_file))
266
+
267
+ if output_file:
268
+ cmd.extend(["-o", str(output_file)])
269
+
270
+ for key, value in flags.items():
271
+ if value is None or value is False:
272
+ continue
273
+ flag = f"--{key.replace('_', '-')}"
274
+ if isinstance(value, bool):
275
+ cmd.append(flag)
276
+ else:
277
+ cmd.extend([flag, str(value)])
278
+
279
+ return self._execute(cmd)
280
+
281
+ # ===== Tool Management =====
282
+
283
+ def tool_run(self, tool: str, args: list[str] | None = None, **flags) -> CommandResult:
284
+ cmd = self._build_command(["tool", "run", tool] + (args or []), **flags)
285
+ return self._execute(cmd)
286
+
287
+ def tool_install(self, tool: str, **flags) -> CommandResult:
288
+ cmd = self._build_command(["tool", "install", tool], **flags)
289
+ return self._execute(cmd)
290
+
291
+ # ===== Python Management =====
292
+
293
+ def python_install(self, version: str, **flags) -> CommandResult:
294
+ cmd = self._build_command(["python", "install", version], **flags)
295
+ return self._execute(cmd)
296
+
297
+ def python_list(self, **flags) -> CommandResult:
298
+ cmd = self._build_command(["python", "list"], **flags)
299
+ return self._execute(cmd)
300
+
301
+ # ===== Utility =====
302
+
303
+ def version(self) -> str:
304
+ result = self._execute([self._binary, "--version"])
305
+ return result.stdout.strip().split()[-1]
306
+
307
+ @property
308
+ def binary(self) -> str:
309
+ return self._binary
310
+
311
+ @property
312
+ def python_executable(self) -> Path:
313
+ if not self._project_env:
314
+ raise ValueError("No project environment configured")
315
+ # TODO: Make this more robust and cross-platform
316
+ if sys.platform == "win32":
317
+ return self._project_env / "Scripts" / "python.exe"
318
+ return self._project_env / "bin" / "python"
@@ -0,0 +1,15 @@
1
+ """Core logging utilities for ComfyDock."""
2
+
3
+ import logging
4
+
5
+
6
+ def get_logger(name: str) -> logging.Logger:
7
+ """Get a logger instance for the given module.
8
+
9
+ Args:
10
+ name: Module name (typically __name__)
11
+
12
+ Returns:
13
+ Configured logger instance
14
+ """
15
+ return logging.getLogger(name)