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.
- comfygit_core/analyzers/custom_node_scanner.py +109 -0
- comfygit_core/analyzers/git_change_parser.py +156 -0
- comfygit_core/analyzers/model_scanner.py +318 -0
- comfygit_core/analyzers/node_classifier.py +58 -0
- comfygit_core/analyzers/node_git_analyzer.py +77 -0
- comfygit_core/analyzers/status_scanner.py +362 -0
- comfygit_core/analyzers/workflow_dependency_parser.py +143 -0
- comfygit_core/caching/__init__.py +16 -0
- comfygit_core/caching/api_cache.py +210 -0
- comfygit_core/caching/base.py +212 -0
- comfygit_core/caching/comfyui_cache.py +100 -0
- comfygit_core/caching/custom_node_cache.py +320 -0
- comfygit_core/caching/workflow_cache.py +797 -0
- comfygit_core/clients/__init__.py +4 -0
- comfygit_core/clients/civitai_client.py +412 -0
- comfygit_core/clients/github_client.py +349 -0
- comfygit_core/clients/registry_client.py +230 -0
- comfygit_core/configs/comfyui_builtin_nodes.py +1614 -0
- comfygit_core/configs/comfyui_models.py +62 -0
- comfygit_core/configs/model_config.py +151 -0
- comfygit_core/constants.py +82 -0
- comfygit_core/core/environment.py +1635 -0
- comfygit_core/core/workspace.py +898 -0
- comfygit_core/factories/environment_factory.py +419 -0
- comfygit_core/factories/uv_factory.py +61 -0
- comfygit_core/factories/workspace_factory.py +109 -0
- comfygit_core/infrastructure/sqlite_manager.py +156 -0
- comfygit_core/integrations/__init__.py +7 -0
- comfygit_core/integrations/uv_command.py +318 -0
- comfygit_core/logging/logging_config.py +15 -0
- comfygit_core/managers/environment_git_orchestrator.py +316 -0
- comfygit_core/managers/environment_model_manager.py +296 -0
- comfygit_core/managers/export_import_manager.py +116 -0
- comfygit_core/managers/git_manager.py +667 -0
- comfygit_core/managers/model_download_manager.py +252 -0
- comfygit_core/managers/model_symlink_manager.py +166 -0
- comfygit_core/managers/node_manager.py +1378 -0
- comfygit_core/managers/pyproject_manager.py +1321 -0
- comfygit_core/managers/user_content_symlink_manager.py +436 -0
- comfygit_core/managers/uv_project_manager.py +569 -0
- comfygit_core/managers/workflow_manager.py +1944 -0
- comfygit_core/models/civitai.py +432 -0
- comfygit_core/models/commit.py +18 -0
- comfygit_core/models/environment.py +293 -0
- comfygit_core/models/exceptions.py +378 -0
- comfygit_core/models/manifest.py +132 -0
- comfygit_core/models/node_mapping.py +201 -0
- comfygit_core/models/protocols.py +248 -0
- comfygit_core/models/registry.py +63 -0
- comfygit_core/models/shared.py +356 -0
- comfygit_core/models/sync.py +42 -0
- comfygit_core/models/system.py +204 -0
- comfygit_core/models/workflow.py +914 -0
- comfygit_core/models/workspace_config.py +71 -0
- comfygit_core/py.typed +0 -0
- comfygit_core/repositories/migrate_paths.py +49 -0
- comfygit_core/repositories/model_repository.py +958 -0
- comfygit_core/repositories/node_mappings_repository.py +246 -0
- comfygit_core/repositories/workflow_repository.py +57 -0
- comfygit_core/repositories/workspace_config_repository.py +121 -0
- comfygit_core/resolvers/global_node_resolver.py +459 -0
- comfygit_core/resolvers/model_resolver.py +250 -0
- comfygit_core/services/import_analyzer.py +218 -0
- comfygit_core/services/model_downloader.py +422 -0
- comfygit_core/services/node_lookup_service.py +251 -0
- comfygit_core/services/registry_data_manager.py +161 -0
- comfygit_core/strategies/__init__.py +4 -0
- comfygit_core/strategies/auto.py +72 -0
- comfygit_core/strategies/confirmation.py +69 -0
- comfygit_core/utils/comfyui_ops.py +125 -0
- comfygit_core/utils/common.py +164 -0
- comfygit_core/utils/conflict_parser.py +232 -0
- comfygit_core/utils/dependency_parser.py +231 -0
- comfygit_core/utils/download.py +216 -0
- comfygit_core/utils/environment_cleanup.py +111 -0
- comfygit_core/utils/filesystem.py +178 -0
- comfygit_core/utils/git.py +1184 -0
- comfygit_core/utils/input_signature.py +145 -0
- comfygit_core/utils/model_categories.py +52 -0
- comfygit_core/utils/pytorch.py +71 -0
- comfygit_core/utils/requirements.py +211 -0
- comfygit_core/utils/retry.py +242 -0
- comfygit_core/utils/symlink_utils.py +119 -0
- comfygit_core/utils/system_detector.py +258 -0
- comfygit_core/utils/uuid.py +28 -0
- comfygit_core/utils/uv_error_handler.py +158 -0
- comfygit_core/utils/version.py +73 -0
- comfygit_core/utils/workflow_hash.py +90 -0
- comfygit_core/validation/resolution_tester.py +297 -0
- comfygit_core-0.2.0.dist-info/METADATA +939 -0
- comfygit_core-0.2.0.dist-info/RECORD +93 -0
- comfygit_core-0.2.0.dist-info/WHEEL +4 -0
- comfygit_core-0.2.0.dist-info/licenses/LICENSE.txt +661 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Custom node cache manager for storing and retrieving downloaded nodes."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from comfygit_core.models.shared import NodeInfo
|
|
11
|
+
|
|
12
|
+
from ..logging.logging_config import get_logger
|
|
13
|
+
from ..utils.common import format_size
|
|
14
|
+
from .base import ContentCacheBase
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CachedNodeInfo:
|
|
21
|
+
"""Information about a cached custom node."""
|
|
22
|
+
|
|
23
|
+
cache_key: str
|
|
24
|
+
name: str
|
|
25
|
+
install_method: str
|
|
26
|
+
url: str
|
|
27
|
+
ref: str | None = None
|
|
28
|
+
cached_at: str = ""
|
|
29
|
+
last_accessed: str = ""
|
|
30
|
+
access_count: int = 0
|
|
31
|
+
size_bytes: int = 0
|
|
32
|
+
content_hash: str | None = None
|
|
33
|
+
source_info: dict | None = None
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
"""Convert to dictionary for JSON serialization."""
|
|
37
|
+
return asdict(self)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, data: dict) -> "CachedNodeInfo":
|
|
41
|
+
"""Create from dictionary."""
|
|
42
|
+
return cls(**data)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CustomNodeCacheManager(ContentCacheBase):
|
|
46
|
+
"""Manages caching of custom node downloads."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, cache_base_path: Path | None = None):
|
|
49
|
+
"""Initialize the cache manager.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
cache_base_path: Base path for cache storage (defaults to platform-specific location)
|
|
53
|
+
"""
|
|
54
|
+
super().__init__("custom_nodes", cache_base_path)
|
|
55
|
+
self.lock_file = self.cache_dir / ".lock"
|
|
56
|
+
|
|
57
|
+
# Load node-specific index
|
|
58
|
+
self.node_index = self._load_node_index()
|
|
59
|
+
|
|
60
|
+
def _load_node_index(self) -> dict[str, CachedNodeInfo]:
|
|
61
|
+
"""Load the node-specific cache index from disk."""
|
|
62
|
+
if not self.index_file.exists():
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with open(self.index_file, encoding='utf-8') as f:
|
|
67
|
+
data = json.load(f)
|
|
68
|
+
|
|
69
|
+
# Convert to CachedNodeInfo objects
|
|
70
|
+
index = {}
|
|
71
|
+
for key, info_dict in data.get("nodes", {}).items():
|
|
72
|
+
try:
|
|
73
|
+
index[key] = CachedNodeInfo.from_dict(info_dict)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.warning(f"Failed to load cache entry {key}: {e}")
|
|
76
|
+
|
|
77
|
+
return index
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Failed to load cache index: {e}")
|
|
81
|
+
return {}
|
|
82
|
+
|
|
83
|
+
def _save_node_index(self):
|
|
84
|
+
"""Save the node-specific cache index to disk."""
|
|
85
|
+
try:
|
|
86
|
+
# Convert to serializable format
|
|
87
|
+
data = {
|
|
88
|
+
"version": "1.0",
|
|
89
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
90
|
+
"nodes": {k: v.to_dict() for k, v in self.node_index.items()},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Write atomically
|
|
94
|
+
temp_file = self.index_file.with_suffix(".tmp")
|
|
95
|
+
with open(temp_file, "w", encoding='utf-8') as f:
|
|
96
|
+
json.dump(data, f, indent=2)
|
|
97
|
+
|
|
98
|
+
# Replace original
|
|
99
|
+
temp_file.replace(self.index_file)
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"Failed to save cache index: {e}")
|
|
103
|
+
|
|
104
|
+
def generate_cache_key(self, node_info: NodeInfo) -> str:
|
|
105
|
+
"""Generate a unique cache key for a custom node.
|
|
106
|
+
|
|
107
|
+
The key is based on:
|
|
108
|
+
- URL
|
|
109
|
+
- Ref (for git repos)
|
|
110
|
+
- Install method
|
|
111
|
+
"""
|
|
112
|
+
# Only add component of node info if not None
|
|
113
|
+
components = [info for info in node_info.__dict__.values() if info is not None]
|
|
114
|
+
key_string = "|".join(components)
|
|
115
|
+
return hashlib.sha256(key_string.encode()).hexdigest()[:16]
|
|
116
|
+
|
|
117
|
+
def is_cached(self, node_info: NodeInfo) -> bool:
|
|
118
|
+
"""Check if a custom node is already cached."""
|
|
119
|
+
cache_key = self.generate_cache_key(node_info)
|
|
120
|
+
|
|
121
|
+
# Check index
|
|
122
|
+
if cache_key not in self.node_index:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
# Verify the actual files exist
|
|
126
|
+
cache_dir = self.store_dir / cache_key
|
|
127
|
+
content_dir = cache_dir / "content"
|
|
128
|
+
|
|
129
|
+
return content_dir.exists() and any(content_dir.iterdir())
|
|
130
|
+
|
|
131
|
+
def get_cached_path(self, node_info: NodeInfo) -> Path | None:
|
|
132
|
+
"""Get the path to cached node content if it exists.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Path to the cached content directory, or None if not cached
|
|
136
|
+
"""
|
|
137
|
+
logger.info(f"Checking if '{node_info.name}' is cached...")
|
|
138
|
+
if not self.is_cached(node_info):
|
|
139
|
+
logger.info(f"'{node_info.name}' is not cached.")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
cache_key = self.generate_cache_key(node_info)
|
|
143
|
+
content_path = self.store_dir / cache_key / "content"
|
|
144
|
+
|
|
145
|
+
logger.debug(f"Generated cached path for '{node_info.name}': {content_path}")
|
|
146
|
+
|
|
147
|
+
# Update access time and count
|
|
148
|
+
if cache_key in self.node_index:
|
|
149
|
+
self.node_index[cache_key].last_accessed = datetime.now(timezone.utc).isoformat()
|
|
150
|
+
self.node_index[cache_key].access_count += 1
|
|
151
|
+
self._save_node_index()
|
|
152
|
+
|
|
153
|
+
return content_path
|
|
154
|
+
|
|
155
|
+
def cache_node(
|
|
156
|
+
self,
|
|
157
|
+
node_info: NodeInfo,
|
|
158
|
+
source_path: Path,
|
|
159
|
+
archive_path: Path | None = None,
|
|
160
|
+
) -> Path:
|
|
161
|
+
"""Cache a custom node from a source directory.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
node_info: The node specification
|
|
165
|
+
source_path: Path to the extracted node content
|
|
166
|
+
archive_path: Optional path to the original archive
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Path to the cached content
|
|
170
|
+
"""
|
|
171
|
+
cache_key = self.generate_cache_key(node_info)
|
|
172
|
+
logger.info(f"Caching custom node: {node_info.name}")
|
|
173
|
+
|
|
174
|
+
# Build metadata for base class
|
|
175
|
+
metadata = {
|
|
176
|
+
"node_spec": asdict(node_info),
|
|
177
|
+
"has_archive": archive_path is not None,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Use base class cache_content method
|
|
181
|
+
content_dir = self.cache_content(cache_key, source_path, metadata)
|
|
182
|
+
|
|
183
|
+
# Copy archive if provided
|
|
184
|
+
if archive_path and archive_path.exists():
|
|
185
|
+
cache_dir = self.store_dir / cache_key
|
|
186
|
+
archive_dest = cache_dir / "archive"
|
|
187
|
+
shutil.copy2(archive_path, archive_dest)
|
|
188
|
+
|
|
189
|
+
# Get size and hash from base class
|
|
190
|
+
cache_dir = self.store_dir / cache_key
|
|
191
|
+
metadata_file = cache_dir / "metadata.json"
|
|
192
|
+
with open(metadata_file, encoding='utf-8') as f:
|
|
193
|
+
full_metadata = json.load(f)
|
|
194
|
+
|
|
195
|
+
# Update node-specific index
|
|
196
|
+
self.node_index[cache_key] = CachedNodeInfo(
|
|
197
|
+
cache_key=cache_key,
|
|
198
|
+
name=node_info.name,
|
|
199
|
+
install_method=node_info.source,
|
|
200
|
+
url=node_info.download_url or node_info.repository or "",
|
|
201
|
+
ref=node_info.version,
|
|
202
|
+
cached_at=full_metadata["cached_at"],
|
|
203
|
+
last_accessed=full_metadata["cached_at"],
|
|
204
|
+
access_count=1,
|
|
205
|
+
size_bytes=full_metadata["size_bytes"],
|
|
206
|
+
content_hash=full_metadata["content_hash"],
|
|
207
|
+
source_info=full_metadata,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
self._save_node_index()
|
|
211
|
+
logger.info(
|
|
212
|
+
f"Cached {node_info.name} ({format_size(full_metadata['size_bytes'])}) with key: {cache_key}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return content_dir
|
|
216
|
+
|
|
217
|
+
def copy_from_cache(self, node_info: NodeInfo, dest_path: Path) -> bool:
|
|
218
|
+
"""Copy a cached node to a destination.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
node_spec: The node specification
|
|
222
|
+
dest_path: Destination path for the node
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if successfully copied, False otherwise
|
|
226
|
+
"""
|
|
227
|
+
cached_path = self.get_cached_path(node_info)
|
|
228
|
+
if not cached_path:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
# Ensure destination parent exists
|
|
233
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
|
|
235
|
+
# Remove destination if it exists
|
|
236
|
+
if dest_path.exists():
|
|
237
|
+
shutil.rmtree(dest_path)
|
|
238
|
+
|
|
239
|
+
# Check if cached content has a single root directory that matches a pattern
|
|
240
|
+
# This handles cases like "ComfyUI-Manager-3.35" inside the cache
|
|
241
|
+
cached_items = list(cached_path.iterdir())
|
|
242
|
+
|
|
243
|
+
if len(cached_items) == 1 and cached_items[0].is_dir():
|
|
244
|
+
# Single directory - check if it's a versioned directory name
|
|
245
|
+
single_dir = cached_items[0]
|
|
246
|
+
dir_name = single_dir.name
|
|
247
|
+
|
|
248
|
+
# Check if this looks like a versioned directory (e.g., "ComfyUI-Manager-3.35")
|
|
249
|
+
# that should be flattened when copying
|
|
250
|
+
base_name = node_info.name
|
|
251
|
+
if dir_name.startswith(base_name) and dir_name != base_name:
|
|
252
|
+
# This is likely a versioned directory that should be flattened
|
|
253
|
+
logger.info(
|
|
254
|
+
f"Flattening nested directory {dir_name} when copying {node_info.name} from cache"
|
|
255
|
+
)
|
|
256
|
+
shutil.copytree(single_dir, dest_path)
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
# Normal copy - preserve structure
|
|
260
|
+
logger.info(f"Copying {node_info.name} from cache to {dest_path}")
|
|
261
|
+
shutil.copytree(cached_path, dest_path)
|
|
262
|
+
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"Failed to copy from cache: {e}")
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
def verify_cache_integrity(self, cache_key: str) -> bool:
|
|
270
|
+
"""Verify the integrity of a cached node."""
|
|
271
|
+
if cache_key not in self.node_index:
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
cache_dir = self.store_dir / cache_key
|
|
275
|
+
content_dir = cache_dir / "content"
|
|
276
|
+
|
|
277
|
+
if not content_dir.exists():
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
# Recalculate hash using base class method
|
|
281
|
+
current_hash = self._calculate_content_hash(content_dir)
|
|
282
|
+
stored_hash = self.node_index[cache_key].content_hash
|
|
283
|
+
|
|
284
|
+
return current_hash == stored_hash
|
|
285
|
+
|
|
286
|
+
def clear_cache(self, node_name: str | None = None) -> int:
|
|
287
|
+
"""Clear cache entries.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
node_name: Specific node name to clear, or None to clear all
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Number of entries cleared
|
|
294
|
+
"""
|
|
295
|
+
cleared = 0
|
|
296
|
+
|
|
297
|
+
if node_name:
|
|
298
|
+
# Clear specific node
|
|
299
|
+
entries_to_clear = [
|
|
300
|
+
(k, v) for k, v in self.node_index.items() if v.name == node_name
|
|
301
|
+
]
|
|
302
|
+
else:
|
|
303
|
+
# Clear all
|
|
304
|
+
entries_to_clear = list(self.node_index.items())
|
|
305
|
+
|
|
306
|
+
for cache_key, _ in entries_to_clear:
|
|
307
|
+
cache_dir = self.store_dir / cache_key
|
|
308
|
+
if cache_dir.exists():
|
|
309
|
+
shutil.rmtree(cache_dir)
|
|
310
|
+
|
|
311
|
+
del self.node_index[cache_key]
|
|
312
|
+
cleared += 1
|
|
313
|
+
|
|
314
|
+
self._save_node_index()
|
|
315
|
+
|
|
316
|
+
return cleared
|
|
317
|
+
|
|
318
|
+
def list_cached_nodes(self) -> list[CachedNodeInfo]:
|
|
319
|
+
"""Get a list of all cached nodes."""
|
|
320
|
+
return list(self.node_index.values())
|