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,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())