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,432 @@
1
+ """CivitAI API data models and enums."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+
7
+ class ModelType(str, Enum):
8
+ """CivitAI model types."""
9
+ CHECKPOINT = "Checkpoint"
10
+ TEXTUAL_INVERSION = "TextualInversion"
11
+ HYPERNETWORK = "Hypernetwork"
12
+ AESTHETIC_GRADIENT = "AestheticGradient"
13
+ LORA = "LORA"
14
+ CONTROLNET = "Controlnet"
15
+ POSES = "Poses"
16
+
17
+
18
+ class SortOrder(str, Enum):
19
+ """Sort order for model search."""
20
+ HIGHEST_RATED = "Highest Rated"
21
+ MOST_DOWNLOADED = "Most Downloaded"
22
+ NEWEST = "Newest"
23
+
24
+
25
+ class TimePeriod(str, Enum):
26
+ """Time period for sorting."""
27
+ ALL_TIME = "AllTime"
28
+ YEAR = "Year"
29
+ MONTH = "Month"
30
+ WEEK = "Week"
31
+ DAY = "Day"
32
+
33
+
34
+ class CommercialUse(str, Enum):
35
+ """Commercial use permissions."""
36
+ NONE = "None"
37
+ IMAGE = "Image"
38
+ RENT = "Rent"
39
+ SELL = "Sell"
40
+
41
+
42
+ class FileFormat(str, Enum):
43
+ """Model file format."""
44
+ SAFETENSOR = "SafeTensor"
45
+ PICKLE_TENSOR = "PickleTensor"
46
+ OTHER = "Other"
47
+
48
+
49
+ class FloatPrecision(str, Enum):
50
+ """Float precision."""
51
+ FP16 = "fp16"
52
+ FP32 = "fp32"
53
+
54
+
55
+ class ModelSize(str, Enum):
56
+ """Model size."""
57
+ FULL = "full"
58
+ PRUNED = "pruned"
59
+
60
+
61
+ @dataclass
62
+ class FileHashes:
63
+ """File hash values for different algorithms."""
64
+ auto_v1: str | None = None
65
+ auto_v2: str | None = None
66
+ sha256: str | None = None
67
+ crc32: str | None = None
68
+ blake3: str | None = None
69
+
70
+ @classmethod
71
+ def from_api_data(cls, data: dict | None) -> "FileHashes | None":
72
+ """Parse from API response."""
73
+ if not data:
74
+ return None
75
+ return cls(
76
+ auto_v1=data.get("AutoV1"),
77
+ auto_v2=data.get("AutoV2"),
78
+ sha256=data.get("SHA256"),
79
+ crc32=data.get("CRC32"),
80
+ blake3=data.get("BLAKE3"),
81
+ )
82
+
83
+
84
+ @dataclass
85
+ class CivitAIFile:
86
+ """Model file information."""
87
+ id: int
88
+ name: str
89
+ size_kb: float
90
+ type: str = "Model"
91
+ pickle_scan_result: str | None = None
92
+ pickle_scan_message: str | None = None
93
+ virus_scan_result: str | None = None
94
+ scanned_at: str | None = None
95
+ primary: bool = False
96
+ download_url: str | None = None
97
+ hashes: FileHashes | None = None
98
+
99
+ # Metadata fields
100
+ fp: FloatPrecision | None = None
101
+ size: ModelSize | None = None
102
+ format: FileFormat | None = None
103
+
104
+ @classmethod
105
+ def from_api_data(cls, data: dict) -> "CivitAIFile":
106
+ """Parse from API response."""
107
+ metadata = data.get("metadata", {})
108
+ return cls(
109
+ id=data.get("id", 0),
110
+ name=data.get("name", ""),
111
+ size_kb=data.get("sizeKB", 0.0),
112
+ type=data.get("type", "Model"),
113
+ pickle_scan_result=data.get("pickleScanResult"),
114
+ pickle_scan_message=data.get("pickleScanMessage"),
115
+ virus_scan_result=data.get("virusScanResult"),
116
+ scanned_at=data.get("scannedAt"),
117
+ primary=data.get("primary", False),
118
+ download_url=data.get("downloadUrl"),
119
+ hashes=FileHashes.from_api_data(data.get("hashes")),
120
+ fp=FloatPrecision(metadata["fp"]) if metadata.get("fp") else None,
121
+ size=ModelSize(metadata["size"]) if metadata.get("size") else None,
122
+ format=FileFormat(metadata["format"]) if metadata.get("format") else None,
123
+ )
124
+
125
+ def get_preferred_hash(self) -> str | None:
126
+ """Get the best available hash for identification (prefers SHA256)."""
127
+ if not self.hashes:
128
+ return None
129
+ return (self.hashes.sha256 or self.hashes.blake3 or
130
+ self.hashes.auto_v2 or self.hashes.auto_v1 or
131
+ self.hashes.crc32)
132
+
133
+
134
+ @dataclass
135
+ class CivitAIImage:
136
+ """Model preview image."""
137
+ id: str
138
+ url: str
139
+ nsfw: bool
140
+ width: int
141
+ height: int
142
+ hash: str
143
+ meta: dict | None = None
144
+
145
+ @classmethod
146
+ def from_api_data(cls, data: dict) -> "CivitAIImage":
147
+ """Parse from API response."""
148
+ return cls(
149
+ id=str(data.get("id", "")),
150
+ url=data.get("url", ""),
151
+ nsfw=bool(data.get("nsfw", False)),
152
+ width=data.get("width", 0),
153
+ height=data.get("height", 0),
154
+ hash=data.get("hash", ""),
155
+ meta=data.get("meta"),
156
+ )
157
+
158
+
159
+ @dataclass
160
+ class CivitAIBasicModelInfo:
161
+ """Basic model information (nested in version response)."""
162
+ name: str
163
+ type: str | None = None
164
+ nsfw: bool = False
165
+ poi: bool = False
166
+
167
+ @classmethod
168
+ def from_api_data(cls, data: dict | None) -> "CivitAIBasicModelInfo | None":
169
+ """Parse from API response."""
170
+ if not data:
171
+ return None
172
+ return cls(
173
+ name=data.get("name", ""),
174
+ type=data.get("type"),
175
+ nsfw=data.get("nsfw", False),
176
+ poi=data.get("poi", False),
177
+ )
178
+
179
+
180
+ @dataclass
181
+ class CivitAIModelVersion:
182
+ """Model version information."""
183
+ id: int
184
+ model_id: int
185
+ name: str
186
+ description: str | None = None
187
+ created_at: str | None = None
188
+ updated_at: str | None = None
189
+ base_model: str | None = None
190
+ early_access_time_frame: int = 0
191
+ download_url: str | None = None
192
+ trained_words: list[str] | None = None
193
+ files: list[CivitAIFile] | None = None
194
+ images: list[CivitAIImage] | None = None
195
+ model: CivitAIBasicModelInfo | None = None
196
+ download_count: int = 0
197
+ rating_count: int = 0
198
+ rating: float = 0.0
199
+
200
+ @classmethod
201
+ def from_api_data(cls, data: dict) -> "CivitAIModelVersion":
202
+ """Parse from API response."""
203
+ stats = data.get("stats", {})
204
+ return cls(
205
+ id=data.get("id", 0),
206
+ model_id=data.get("modelId", 0),
207
+ name=data.get("name", ""),
208
+ description=data.get("description"),
209
+ created_at=data.get("createdAt"),
210
+ updated_at=data.get("updatedAt"),
211
+ base_model=data.get("baseModel"),
212
+ early_access_time_frame=data.get("earlyAccessTimeFrame", 0),
213
+ download_url=data.get("downloadUrl"),
214
+ trained_words=data.get("trainedWords", []),
215
+ files=[CivitAIFile.from_api_data(f) for f in data.get("files", [])],
216
+ images=[CivitAIImage.from_api_data(i) for i in data.get("images", [])],
217
+ model=CivitAIBasicModelInfo.from_api_data(data.get("model")),
218
+ download_count=stats.get("downloadCount", 0),
219
+ rating_count=stats.get("ratingCount", 0),
220
+ rating=stats.get("rating", 0.0),
221
+ )
222
+
223
+
224
+ @dataclass
225
+ class CivitAICreator:
226
+ """Model creator information."""
227
+ username: str
228
+ image: str | None = None
229
+
230
+ @classmethod
231
+ def from_api_data(cls, data: dict) -> "CivitAICreator":
232
+ """Parse from API response."""
233
+ return cls(
234
+ username=data.get("username", ""),
235
+ image=data.get("image"),
236
+ )
237
+
238
+
239
+ @dataclass
240
+ class CivitAIModel:
241
+ """CivitAI model information."""
242
+ id: int
243
+ name: str
244
+ description: str | None = None
245
+ type: ModelType | None = None
246
+ nsfw: bool = False
247
+ tags: list[str] | None = None
248
+ mode: str | None = None # "Archived" or "TakenDown"
249
+ creator: CivitAICreator | None = None
250
+ model_versions: list[CivitAIModelVersion] | None = None
251
+
252
+ # Stats
253
+ download_count: int = 0
254
+ favorite_count: int = 0
255
+ comment_count: int = 0
256
+ rating_count: int = 0
257
+ rating: float = 0.0
258
+
259
+ @classmethod
260
+ def from_api_data(cls, data: dict) -> "CivitAIModel":
261
+ """Parse from API response."""
262
+ stats = data.get("stats", {})
263
+ creator_data = data.get("creator")
264
+
265
+ # Handle tags that can be either strings or objects with 'name' field
266
+ tags_raw = data.get("tags", [])
267
+ tags = []
268
+ for tag in tags_raw:
269
+ if isinstance(tag, str):
270
+ tags.append(tag)
271
+ elif isinstance(tag, dict) and "name" in tag:
272
+ tags.append(tag["name"])
273
+
274
+ return cls(
275
+ id=data.get("id", 0),
276
+ name=data.get("name", ""),
277
+ description=data.get("description"),
278
+ type=ModelType(data["type"]) if data.get("type") else None,
279
+ nsfw=data.get("nsfw", False),
280
+ tags=tags,
281
+ mode=data.get("mode"),
282
+ creator=CivitAICreator.from_api_data(creator_data) if creator_data else None,
283
+ model_versions=[
284
+ CivitAIModelVersion.from_api_data(v)
285
+ for v in data.get("modelVersions", [])
286
+ ],
287
+ download_count=stats.get("downloadCount", 0),
288
+ favorite_count=stats.get("favoriteCount", 0),
289
+ comment_count=stats.get("commentCount", 0),
290
+ rating_count=stats.get("ratingCount", 0),
291
+ rating=stats.get("rating", 0.0),
292
+ )
293
+
294
+ def get_latest_version(self) -> CivitAIModelVersion | None:
295
+ """Get the most recent model version."""
296
+ if not self.model_versions:
297
+ return None
298
+ return self.model_versions[0] # API returns newest first
299
+
300
+ def get_primary_file(self) -> CivitAIFile | None:
301
+ """Get the primary file from the latest version."""
302
+ latest = self.get_latest_version()
303
+ if not latest or not latest.files:
304
+ return None
305
+
306
+ # Find primary file or default to first
307
+ for file in latest.files:
308
+ if file.primary:
309
+ return file
310
+ return latest.files[0]
311
+
312
+ def find_file_by_hash(self, hash_value: str) -> CivitAIFile | None:
313
+ """Find a file across all versions by any of its hashes."""
314
+ if not self.model_versions:
315
+ return None
316
+
317
+ hash_upper = hash_value.upper()
318
+ for version in self.model_versions:
319
+ if not version.files:
320
+ continue
321
+ for file in version.files:
322
+ if not file.hashes:
323
+ continue
324
+ if (file.hashes.auto_v1 == hash_upper or
325
+ file.hashes.auto_v2 == hash_upper or
326
+ file.hashes.sha256 == hash_upper or
327
+ file.hashes.crc32 == hash_upper or
328
+ file.hashes.blake3 == hash_upper):
329
+ return file
330
+ return None
331
+
332
+
333
+ @dataclass
334
+ class SearchParams:
335
+ """Parameters for model search."""
336
+ query: str | None = None
337
+ tag: str | None = None
338
+ username: str | None = None
339
+ types: list[ModelType] | None = None
340
+ sort: SortOrder | None = None
341
+ period: TimePeriod | None = None
342
+ limit: int = 20
343
+ page: int = 1
344
+ nsfw: bool | None = None
345
+ commercial_use: CommercialUse | None = None
346
+ allow_no_credit: bool | None = None
347
+ allow_derivatives: bool | None = None
348
+ allow_different_licenses: bool | None = None
349
+ primary_file_only: bool | None = None
350
+ supports_generation: bool | None = None
351
+
352
+ def to_dict(self) -> dict:
353
+ """Convert to query parameters dict."""
354
+ params = {}
355
+
356
+ if self.query:
357
+ params["query"] = self.query
358
+ if self.tag:
359
+ params["tag"] = self.tag
360
+ if self.username:
361
+ params["username"] = self.username
362
+ if self.types:
363
+ # CivitAI expects multiple types as comma-separated string
364
+ params["types"] = ",".join([t.value for t in self.types])
365
+ if self.sort:
366
+ params["sort"] = self.sort.value
367
+ if self.period:
368
+ params["period"] = self.period.value
369
+ if self.limit:
370
+ params["limit"] = self.limit
371
+ # Only include page if not using query (CivitAI restriction)
372
+ if self.page and self.page > 1 and not self.query:
373
+ params["page"] = self.page
374
+ if self.nsfw is not None:
375
+ params["nsfw"] = str(self.nsfw).lower()
376
+ if self.commercial_use:
377
+ params["allowCommercialUse"] = self.commercial_use.value
378
+ if self.allow_no_credit is not None:
379
+ params["allowNoCredit"] = str(self.allow_no_credit).lower()
380
+ if self.allow_derivatives is not None:
381
+ params["allowDerivatives"] = str(self.allow_derivatives).lower()
382
+ if self.allow_different_licenses is not None:
383
+ params["allowDifferentLicenses"] = str(self.allow_different_licenses).lower()
384
+ if self.primary_file_only is not None:
385
+ params["primaryFileOnly"] = str(self.primary_file_only).lower()
386
+ if self.supports_generation is not None:
387
+ params["supportsGeneration"] = str(self.supports_generation).lower()
388
+
389
+ return params
390
+
391
+
392
+ @dataclass
393
+ class CivitAITag:
394
+ """Tag information."""
395
+ name: str
396
+ model_count: int
397
+ link: str
398
+
399
+ @classmethod
400
+ def from_api_data(cls, data: dict) -> "CivitAITag":
401
+ """Parse from API response."""
402
+ return cls(
403
+ name=data.get("name", ""),
404
+ model_count=data.get("modelCount", 0),
405
+ link=data.get("link", ""),
406
+ )
407
+
408
+
409
+ @dataclass
410
+ class SearchResponse:
411
+ """Model search response with pagination."""
412
+ items: list[CivitAIModel]
413
+ total_items: int
414
+ current_page: int
415
+ page_size: int
416
+ total_pages: int
417
+ next_page: str | None = None
418
+ prev_page: str | None = None
419
+
420
+ @classmethod
421
+ def from_api_data(cls, data: dict) -> "SearchResponse":
422
+ """Parse from API response."""
423
+ metadata = data.get("metadata", {})
424
+ return cls(
425
+ items=[CivitAIModel.from_api_data(m) for m in data.get("items", [])],
426
+ total_items=int(metadata.get("totalItems", 0)),
427
+ current_page=int(metadata.get("currentPage", 1)),
428
+ page_size=int(metadata.get("pageSize", 20)),
429
+ total_pages=int(metadata.get("totalPages", 1)),
430
+ next_page=metadata.get("nextPage"),
431
+ prev_page=metadata.get("prevPage"),
432
+ )
@@ -0,0 +1,18 @@
1
+ """Commit operation result models."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from ..models.shared import ModelWithLocation
9
+
10
+ @dataclass
11
+ class ModelResolutionRequest:
12
+ """Request for resolving ambiguous model matches."""
13
+ workflow_name: str
14
+ node_id: str
15
+ node_type: str
16
+ widget_index: int
17
+ original_value: str
18
+ candidates: list[ModelWithLocation]