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