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,4 @@
1
+ from .registry_client import ComfyRegistryClient
2
+ from .github_client import GitHubClient, GitHubRelease, GitHubRepoInfo
3
+
4
+ __all__ = ["ComfyRegistryClient", "GitHubClient", "GitHubRelease", "GitHubRepoInfo"]
@@ -0,0 +1,412 @@
1
+ """CivitAI API client for model discovery, metadata, and downloads."""
2
+
3
+ import json
4
+ import os
5
+ import urllib.error
6
+ import urllib.parse
7
+ import urllib.request
8
+ from collections.abc import Iterator
9
+ from typing import Any
10
+
11
+ from comfygit_core.caching.api_cache import APICacheManager
12
+ from comfygit_core.logging.logging_config import get_logger
13
+ from comfygit_core.models.civitai import (
14
+ CivitAIModel,
15
+ CivitAIModelVersion,
16
+ CivitAITag,
17
+ SearchParams,
18
+ SearchResponse,
19
+ )
20
+ from comfygit_core.models.exceptions import (
21
+ CDRegistryAuthError,
22
+ CDRegistryConnectionError,
23
+ CDRegistryError,
24
+ CDRegistryServerError,
25
+ )
26
+ from comfygit_core.repositories.workspace_config_repository import (
27
+ WorkspaceConfigRepository,
28
+ )
29
+ from comfygit_core.utils.retry import (
30
+ RateLimitManager,
31
+ RetryConfig,
32
+ retry_on_rate_limit,
33
+ )
34
+
35
+ logger = get_logger(__name__)
36
+
37
+ DEFAULT_CIVITAI_URL = "https://civitai.com/api/v1"
38
+
39
+
40
+ class CivitAIError(CDRegistryError):
41
+ """Base CivitAI exception."""
42
+ pass
43
+
44
+
45
+ class CivitAINotFoundError(CivitAIError):
46
+ """Model or version not found."""
47
+ pass
48
+
49
+
50
+ class CivitAIRateLimitError(CivitAIError):
51
+ """Hit CivitAI rate limits."""
52
+ pass
53
+
54
+
55
+ class CivitAIClient:
56
+ """Client for interacting with CivitAI API.
57
+
58
+ Provides model discovery, metadata retrieval, and download URL generation.
59
+ Supports optional authentication for restricted content.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ cache_manager: APICacheManager,
65
+ api_key: str | None = None,
66
+ workspace_config: WorkspaceConfigRepository | None = None,
67
+ base_url: str = DEFAULT_CIVITAI_URL,
68
+ ):
69
+ """Initialize CivitAI client.
70
+
71
+ Args:
72
+ cache_manager: Required cache manager for API responses
73
+ api_key: Direct API key override
74
+ workspace_config: Workspace config repository for API key lookup
75
+ base_url: CivitAI API base URL
76
+ """
77
+ self.base_url = base_url
78
+ self.cache_manager = cache_manager
79
+ self.workspace_config = workspace_config
80
+
81
+ # Resolve API key: direct > environment > config
82
+ self._api_key = api_key
83
+ if not self._api_key:
84
+ self._api_key = os.environ.get("CIVITAI_API_TOKEN")
85
+ if not self._api_key and workspace_config:
86
+ self._api_key = workspace_config.get_civitai_token()
87
+
88
+ self.rate_limiter = RateLimitManager(min_interval=0.1)
89
+ self.retry_config = RetryConfig(
90
+ max_retries=3,
91
+ initial_delay=0.5,
92
+ max_delay=30.0,
93
+ exponential_base=2.0,
94
+ jitter=True,
95
+ )
96
+
97
+ def search_models(
98
+ self, params: SearchParams | None = None, **kwargs
99
+ ) -> SearchResponse:
100
+ """Search for models with filters.
101
+
102
+ Args:
103
+ params: SearchParams object with filters
104
+ **kwargs: Alternative way to pass search parameters
105
+
106
+ Returns:
107
+ SearchResponse with models and pagination info
108
+ """
109
+ if params:
110
+ query_params = params.to_dict()
111
+ else:
112
+ # Build from kwargs
113
+ query_params = {}
114
+ for key, value in kwargs.items():
115
+ if value is not None:
116
+ query_params[key] = value
117
+
118
+ url = f"{self.base_url}/models"
119
+ if query_params:
120
+ url += f"?{urllib.parse.urlencode(query_params)}"
121
+
122
+ data = self._make_request(url)
123
+ if data:
124
+ return SearchResponse.from_api_data(data)
125
+
126
+ return SearchResponse(
127
+ items=[], total_items=0, current_page=1, page_size=0, total_pages=0
128
+ )
129
+
130
+ def search_models_iter(
131
+ self, params: SearchParams | None = None, **kwargs
132
+ ) -> Iterator[CivitAIModel]:
133
+ """Iterate through all search results with automatic pagination.
134
+
135
+ Args:
136
+ params: SearchParams object with filters
137
+ **kwargs: Alternative way to pass search parameters
138
+
139
+ Yields:
140
+ CivitAIModel objects
141
+ """
142
+ current_params = params or SearchParams(**kwargs)
143
+ current_params.page = 1
144
+
145
+ while True:
146
+ response = self.search_models(current_params)
147
+ if not response.items:
148
+ break
149
+
150
+ yield from response.items
151
+
152
+ # Check if there's a next page
153
+ if current_params.page >= response.total_pages:
154
+ break
155
+
156
+ current_params.page += 1
157
+
158
+ def get_model(self, model_id: int) -> CivitAIModel | None:
159
+ """Get model by ID.
160
+
161
+ Args:
162
+ model_id: CivitAI model ID
163
+
164
+ Returns:
165
+ Model info or None if not found
166
+ """
167
+ url = f"{self.base_url}/models/{model_id}"
168
+ data = self._make_request(url)
169
+
170
+ if data:
171
+ logger.info(f"Found CivitAI model {model_id}")
172
+ return CivitAIModel.from_api_data(data)
173
+
174
+ return None
175
+
176
+ def get_model_version(self, version_id: int) -> CivitAIModelVersion | None:
177
+ """Get specific model version.
178
+
179
+ Args:
180
+ version_id: Model version ID
181
+
182
+ Returns:
183
+ Version info or None if not found
184
+ """
185
+ url = f"{self.base_url}/model-versions/{version_id}"
186
+ data = self._make_request(url)
187
+
188
+ if data:
189
+ logger.info(f"Found CivitAI model version {version_id}")
190
+ return CivitAIModelVersion.from_api_data(data)
191
+
192
+ return None
193
+
194
+ def get_model_by_hash(
195
+ self, hash_value: str, algorithm: str | None = None
196
+ ) -> CivitAIModelVersion | None:
197
+ """Get model version by file hash.
198
+
199
+ Args:
200
+ hash_value: File hash
201
+ algorithm: Hash algorithm (auto-detected if not provided)
202
+
203
+ Returns:
204
+ Version info or None if not found
205
+
206
+ Supported algorithms: AutoV1, AutoV2, SHA256, CRC32, Blake3
207
+ """
208
+ if not algorithm:
209
+ algorithm = self._detect_hash_algorithm(hash_value)
210
+ logger.debug(f"Auto-detected hash algorithm: {algorithm}")
211
+
212
+ url = f"{self.base_url}/model-versions/by-hash/{hash_value}"
213
+ data = self._make_request(url)
214
+
215
+ if data:
216
+ logger.info(f"Found model by hash {hash_value[:8]}...")
217
+ return CivitAIModelVersion.from_api_data(data)
218
+
219
+ return None
220
+
221
+ def get_download_url(
222
+ self,
223
+ version_id: int,
224
+ file_format: str | None = None,
225
+ size: str | None = None,
226
+ fp: str | None = None,
227
+ ) -> str:
228
+ """Generate download URL for a model version.
229
+
230
+ Args:
231
+ version_id: Model version ID
232
+ file_format: Desired format (SafeTensor, PickleTensor)
233
+ size: Model size (full, pruned)
234
+ fp: Float precision (fp16, fp32)
235
+
236
+ Returns:
237
+ Download URL with authentication if configured
238
+
239
+ Note: The actual download will redirect to a pre-signed S3 URL
240
+ """
241
+ params: dict[str, str] = {}
242
+ if file_format:
243
+ params["format"] = file_format
244
+ if size:
245
+ params["size"] = size
246
+ if fp:
247
+ params["fp"] = fp
248
+
249
+ base = f"https://civitai.com/api/download/models/{version_id}"
250
+ if params:
251
+ base += f"?{urllib.parse.urlencode(params)}"
252
+
253
+ # Add API token if available (required for some models)
254
+ if self._api_key:
255
+ separator = "&" if "?" in base else "?"
256
+ base += f"{separator}token={self._api_key}"
257
+
258
+ return base
259
+
260
+ def get_tags(
261
+ self, query: str | None = None, limit: int = 20
262
+ ) -> list[CivitAITag]:
263
+ """Get tags optionally filtered by query.
264
+
265
+ Args:
266
+ query: Optional search term for tags
267
+ limit: Maximum number of tags to return
268
+
269
+ Returns:
270
+ List of tags
271
+ """
272
+ params: dict[str, Any] = {"limit": limit}
273
+ if query:
274
+ params["query"] = query
275
+
276
+ url = f"{self.base_url}/tags?{urllib.parse.urlencode(params)}"
277
+ data = self._make_request(url)
278
+
279
+ if data and "items" in data:
280
+ return [CivitAITag.from_api_data(t) for t in data["items"]]
281
+
282
+ return []
283
+
284
+ def _detect_hash_algorithm(self, hash_value: str) -> str:
285
+ """Auto-detect hash algorithm by length and pattern.
286
+
287
+ Args:
288
+ hash_value: Hash string
289
+
290
+ Returns:
291
+ Detected algorithm name
292
+ """
293
+ hash_len = len(hash_value)
294
+
295
+ if hash_len == 8:
296
+ return "CRC32"
297
+ elif hash_len == 10:
298
+ return "AutoV1"
299
+ elif hash_len == 12:
300
+ return "AutoV2"
301
+ elif hash_len == 64:
302
+ return "SHA256"
303
+ elif hash_len == 128:
304
+ return "Blake3"
305
+ else:
306
+ # Default to SHA256
307
+ logger.warning(f"Unknown hash length {hash_len}, assuming SHA256")
308
+ return "SHA256"
309
+
310
+ @retry_on_rate_limit(RetryConfig(max_retries=3, initial_delay=0.5, max_delay=30.0))
311
+ def _make_request(self, url: str, authenticated: bool = False) -> dict | None:
312
+ """Make a request to CivitAI API with retry logic.
313
+
314
+ Args:
315
+ url: Request URL
316
+ authenticated: Force authentication for this request
317
+
318
+ Returns:
319
+ Response data or None for 404
320
+
321
+ Raises:
322
+ CivitAIRateLimitError: For rate limit errors
323
+ CDRegistryAuthError: For authentication issues
324
+ CDRegistryServerError: For server errors
325
+ CDRegistryConnectionError: For network issues
326
+ """
327
+ # Check cache first
328
+ cache_key = url
329
+ if self._api_key and authenticated:
330
+ # Include auth state in cache key
331
+ cache_key = f"{url}:authed"
332
+
333
+ cached_data = self.cache_manager.get("civitai", cache_key)
334
+ if cached_data is not None:
335
+ logger.debug("Using cached data for CivitAI request")
336
+ return cached_data
337
+
338
+ # Rate limit ourselves
339
+ self.rate_limiter.wait_if_needed("civitai_api")
340
+
341
+ # Build request
342
+ req = urllib.request.Request(url)
343
+ req.add_header("User-Agent", "ComfyDock/1.0")
344
+ req.add_header("Content-Type", "application/json")
345
+
346
+ # Add authentication if available
347
+ if (authenticated or self._api_key) and self._api_key:
348
+ req.add_header("Authorization", f"Bearer {self._api_key}")
349
+
350
+ try:
351
+ with urllib.request.urlopen(req, timeout=30) as response:
352
+ if response.status == 200:
353
+ json_data = json.loads(response.read().decode("utf-8"))
354
+ # Cache successful responses
355
+ self.cache_manager.set("civitai", cache_key, json_data)
356
+ return json_data
357
+
358
+ except urllib.error.HTTPError as e:
359
+ if e.code == 404:
360
+ logger.debug(f"CivitAI: Not found at '{url}'")
361
+ return None
362
+
363
+ elif e.code == 429:
364
+ # Rate limit
365
+ logger.warning("CivitAI rate limit hit")
366
+ raise CivitAIRateLimitError("Rate limit exceeded") from e
367
+
368
+ elif e.code in (401, 403):
369
+ # Authentication/authorization errors
370
+ logger.error(f"CivitAI auth error: HTTP {e.code}")
371
+
372
+ error_msg = f"CivitAI authentication failed (HTTP {e.code})"
373
+ if e.code == 401 and not self._api_key:
374
+ error_msg += " - API key may be required for this resource"
375
+
376
+ raise CDRegistryAuthError(error_msg) from e
377
+
378
+ elif e.code >= 500:
379
+ # Server errors
380
+ logger.error(f"CivitAI server error: HTTP {e.code}")
381
+ raise CDRegistryServerError(
382
+ f"CivitAI server error (HTTP {e.code})"
383
+ ) from e
384
+
385
+ else:
386
+ # Other HTTP errors
387
+ error_detail = ""
388
+ try:
389
+ error_data = e.read().decode("utf-8")
390
+ if error_data:
391
+ error_detail = f" - {error_data}"
392
+ except:
393
+ pass
394
+ logger.error(f"CivitAI HTTP error: {e.code} {e.reason}{error_detail}")
395
+ logger.debug(f"Failed URL: {url}")
396
+ raise CivitAIError(
397
+ f"CivitAI request failed: HTTP {e.code} {e.reason}{error_detail}"
398
+ ) from e
399
+
400
+ except urllib.error.URLError as e:
401
+ # Network errors
402
+ logger.error(f"CivitAI connection error: {e}")
403
+ raise CDRegistryConnectionError(
404
+ f"Failed to connect to CivitAI: {e.reason}"
405
+ ) from e
406
+
407
+ except Exception as e:
408
+ # Unexpected errors
409
+ logger.error(f"Unexpected error accessing CivitAI: {e}")
410
+ raise CivitAIError(f"CivitAI request failed: {e}") from e
411
+
412
+ return None