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,349 @@
1
+ """GitHub API client for repository operations and metadata retrieval."""
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.request
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from comfygit_core.caching.api_cache import APICacheManager
10
+ from comfygit_core.constants import DEFAULT_GITHUB_URL
11
+ from comfygit_core.logging.logging_config import get_logger
12
+ from comfygit_core.utils.git import parse_github_url
13
+ from comfygit_core.utils.retry import RateLimitManager, RetryConfig
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class GitHubRepoInfo:
20
+ """Information about a GitHub repository."""
21
+ owner: str
22
+ name: str
23
+ default_branch: str
24
+ description: str | None = None
25
+ latest_release: str | None = None
26
+ clone_url: str | None = None
27
+ latest_commit: str | None = None
28
+
29
+
30
+ @dataclass
31
+ class GitHubRelease:
32
+ """Single GitHub release."""
33
+ tag_name: str
34
+ name: str
35
+ published_at: str
36
+ prerelease: bool
37
+ draft: bool
38
+ html_url: str
39
+
40
+
41
+ class GitHubClient:
42
+ """Client for interacting with GitHub repositories.
43
+
44
+ Provides repository cloning, metadata retrieval, and release management.
45
+ Designed for custom nodes hosted on GitHub.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ cache_manager: APICacheManager,
51
+ base_url: str = DEFAULT_GITHUB_URL,
52
+ ):
53
+ self.base_url = base_url
54
+ self.cache_manager = cache_manager
55
+ self.rate_limiter = RateLimitManager(min_interval=0.05)
56
+ self.retry_config = RetryConfig(
57
+ max_retries=3,
58
+ initial_delay=1.0,
59
+ max_delay=30.0,
60
+ exponential_base=2.0,
61
+ jitter=True,
62
+ )
63
+
64
+ def parse_github_url(self, url: str) -> GitHubRepoInfo | None:
65
+ """Parse a GitHub URL to extract repository information.
66
+
67
+ Args:
68
+ url: GitHub repository URL
69
+
70
+ Returns:
71
+ GitHubRepoInfo or None if invalid URL
72
+ """
73
+ parsed = parse_github_url(url)
74
+ if not parsed:
75
+ return None
76
+
77
+ owner, name, _ = parsed # Ignore commit for basic parsing
78
+ return GitHubRepoInfo(
79
+ owner=owner,
80
+ name=name,
81
+ default_branch="main", # Will be updated by get_repository_info
82
+ clone_url=f"https://github.com/{owner}/{name}.git"
83
+ )
84
+
85
+ def clone_repository(self, repo_url: str, target_path: Path,
86
+ ref: str | None = None) -> bool:
87
+ """Clone a GitHub repository to a target path.
88
+
89
+ Args:
90
+ repo_url: GitHub repository URL
91
+ target_path: Where to clone the repository
92
+ ref: Optional git ref (branch/tag/commit) to checkout
93
+
94
+ Returns:
95
+ True if successful, False otherwise
96
+ """
97
+ # TODO: Use git to clone repository
98
+ # TODO: Checkout specific ref if provided
99
+ # TODO: Handle authentication if needed
100
+ return False
101
+
102
+ def get_repository_info(self, repo_url: str, ref: str | None = None) -> GitHubRepoInfo | None:
103
+ """Get information about a GitHub repository.
104
+
105
+ Args:
106
+ repo_url: GitHub repository URL
107
+ ref: Optional git ref (branch/tag/commit) to resolve
108
+
109
+ Returns:
110
+ Repository information or None if not found
111
+ """
112
+ parsed = parse_github_url(repo_url)
113
+ if not parsed:
114
+ return None
115
+
116
+ owner, name, url_commit = parsed
117
+
118
+ # ref parameter takes precedence over URL-embedded commit
119
+ target_ref = ref or url_commit
120
+ cache_key = f"{owner}/{name}" + (f"@{target_ref}" if target_ref else "")
121
+
122
+ # Try cache first
123
+ cached = self.cache_manager.get("github", cache_key)
124
+ if cached:
125
+ return GitHubRepoInfo(**cached)
126
+
127
+ try:
128
+ # Rate limit API calls
129
+ self.rate_limiter.wait_if_needed("github_api")
130
+
131
+ # Get repo metadata
132
+ api_url = f"https://api.github.com/repos/{owner}/{name}"
133
+ with urllib.request.urlopen(api_url) as response:
134
+ repo_data = json.loads(response.read())
135
+
136
+ default_branch = repo_data.get("default_branch", "main")
137
+
138
+ # Resolve target ref to commit SHA
139
+ latest_commit = None
140
+ if target_ref:
141
+ # Ref specified - resolve to commit (works for branches, tags, and commits)
142
+ try:
143
+ commits_url = f"https://api.github.com/repos/{owner}/{name}/commits/{target_ref}"
144
+ with urllib.request.urlopen(commits_url) as response:
145
+ commit_data = json.loads(response.read())
146
+ latest_commit = commit_data.get("sha")
147
+ except urllib.error.HTTPError:
148
+ logger.warning(f"Could not resolve ref '{target_ref}' for {owner}/{name}")
149
+ pass
150
+ else:
151
+ # No ref - get latest commit from default branch
152
+ try:
153
+ commits_url = f"https://api.github.com/repos/{owner}/{name}/commits/{default_branch}"
154
+ with urllib.request.urlopen(commits_url) as response:
155
+ commit_data = json.loads(response.read())
156
+ latest_commit = commit_data.get("sha")
157
+ except urllib.error.HTTPError:
158
+ pass
159
+
160
+ # Get latest release
161
+ latest_release = None
162
+ try:
163
+ releases_url = f"https://api.github.com/repos/{owner}/{name}/releases/latest"
164
+ with urllib.request.urlopen(releases_url) as response:
165
+ release_data = json.loads(response.read())
166
+ latest_release = release_data.get("tag_name")
167
+ except urllib.error.HTTPError:
168
+ # No releases found, that's okay
169
+ pass
170
+
171
+ repo_info = GitHubRepoInfo(
172
+ owner=owner,
173
+ name=name,
174
+ default_branch=default_branch,
175
+ description=repo_data.get("description"),
176
+ latest_release=latest_release,
177
+ clone_url=repo_data.get("clone_url"),
178
+ latest_commit=latest_commit
179
+ )
180
+
181
+ # Cache the result
182
+ self.cache_manager.set("github", cache_key, repo_info.__dict__)
183
+
184
+ return repo_info
185
+
186
+ except (urllib.error.URLError, json.JSONDecodeError) as e:
187
+ logger.warning(f"Failed to get repository info for {repo_url}: {e}")
188
+ return None
189
+
190
+ def list_releases(self, repo_url: str, include_prerelease: bool = False,
191
+ limit: int = 10) -> list[GitHubRelease]:
192
+ """List releases from GitHub API.
193
+
194
+ Args:
195
+ repo_url: GitHub repository URL
196
+ include_prerelease: Include pre-release versions
197
+ limit: Maximum number of releases to fetch
198
+
199
+ Returns:
200
+ List of GitHubRelease objects, sorted by date (newest first)
201
+ """
202
+ parsed = parse_github_url(repo_url)
203
+ if not parsed:
204
+ return []
205
+
206
+ owner, name, _ = parsed
207
+ cache_key = f"{owner}/{name}/releases"
208
+
209
+ # Try cache first
210
+ cached = self.cache_manager.get("github", cache_key)
211
+ if cached:
212
+ releases = [GitHubRelease(**r) for r in cached]
213
+ # Filter and limit
214
+ if not include_prerelease:
215
+ releases = [r for r in releases if not r.prerelease]
216
+ return releases[:limit]
217
+
218
+ try:
219
+ # Rate limit API calls
220
+ self.rate_limiter.wait_if_needed("github_api")
221
+
222
+ # Get all releases
223
+ api_url = f"https://api.github.com/repos/{owner}/{name}/releases?per_page={min(limit * 2, 100)}"
224
+ with urllib.request.urlopen(api_url) as response:
225
+ releases_data = json.loads(response.read())
226
+
227
+ # Parse into GitHubRelease objects
228
+ releases = []
229
+ for release_data in releases_data:
230
+ # Skip drafts always
231
+ if release_data.get("draft", False):
232
+ continue
233
+
234
+ release = GitHubRelease(
235
+ tag_name=release_data["tag_name"],
236
+ name=release_data.get("name", release_data["tag_name"]),
237
+ published_at=release_data["published_at"],
238
+ prerelease=release_data.get("prerelease", False),
239
+ draft=release_data.get("draft", False),
240
+ html_url=release_data["html_url"]
241
+ )
242
+ releases.append(release)
243
+
244
+ # Sort by published date (newest first)
245
+ releases.sort(key=lambda r: r.published_at, reverse=True)
246
+
247
+ # Cache all releases
248
+ self.cache_manager.set("github", cache_key, [r.__dict__ for r in releases])
249
+
250
+ # Filter and limit
251
+ if not include_prerelease:
252
+ releases = [r for r in releases if not r.prerelease]
253
+ return releases[:limit]
254
+
255
+ except (urllib.error.URLError, json.JSONDecodeError) as e:
256
+ logger.warning(f"Failed to list releases for {repo_url}: {e}")
257
+ return []
258
+
259
+ def get_release_by_tag(self, repo_url: str, tag: str) -> GitHubRelease | None:
260
+ """Get specific release by tag name.
261
+
262
+ Args:
263
+ repo_url: GitHub repository URL
264
+ tag: Release tag (e.g., "v0.3.20")
265
+
266
+ Returns:
267
+ GitHubRelease if found, None otherwise
268
+ """
269
+ parsed = parse_github_url(repo_url)
270
+ if not parsed:
271
+ return None
272
+
273
+ owner, name, _ = parsed
274
+ cache_key = f"{owner}/{name}/release/{tag}"
275
+
276
+ # Try cache first
277
+ cached = self.cache_manager.get("github", cache_key)
278
+ if cached:
279
+ return GitHubRelease(**cached)
280
+
281
+ try:
282
+ # Rate limit API calls
283
+ self.rate_limiter.wait_if_needed("github_api")
284
+
285
+ # Get specific release by tag
286
+ api_url = f"https://api.github.com/repos/{owner}/{name}/releases/tags/{tag}"
287
+ with urllib.request.urlopen(api_url) as response:
288
+ release_data = json.loads(response.read())
289
+
290
+ release = GitHubRelease(
291
+ tag_name=release_data["tag_name"],
292
+ name=release_data.get("name", release_data["tag_name"]),
293
+ published_at=release_data["published_at"],
294
+ prerelease=release_data.get("prerelease", False),
295
+ draft=release_data.get("draft", False),
296
+ html_url=release_data["html_url"]
297
+ )
298
+
299
+ # Cache the result
300
+ self.cache_manager.set("github", cache_key, release.__dict__)
301
+
302
+ return release
303
+
304
+ except urllib.error.HTTPError as e:
305
+ if e.code == 404:
306
+ # Note negative result (don't cache to avoid stale data)
307
+ return None
308
+ logger.warning(f"Failed to get release {tag} for {repo_url}: {e}")
309
+ return None
310
+ except (urllib.error.URLError, json.JSONDecodeError) as e:
311
+ logger.warning(f"Failed to get release {tag} for {repo_url}: {e}")
312
+ return None
313
+
314
+ def validate_version_exists(self, repo_url: str, version: str) -> bool:
315
+ """Check if a version (tag, commit, or branch) exists.
316
+
317
+ Args:
318
+ repo_url: GitHub repository URL
319
+ version: Tag name, commit SHA, or branch name
320
+
321
+ Returns:
322
+ True if version exists and is accessible
323
+ """
324
+ # If looks like tag (starts with 'v'), check releases
325
+ if version.startswith('v'):
326
+ release = self.get_release_by_tag(repo_url, version)
327
+ return release is not None
328
+
329
+ # For branches and commits, we could check other APIs
330
+ # but for simplicity, assume they exist
331
+ # (git clone will fail if they don't)
332
+ return True
333
+
334
+ def download_release_asset(self, repo_url: str, asset_name: str,
335
+ target_path: Path) -> bool:
336
+ """Download a specific release asset from a repository.
337
+
338
+ Args:
339
+ repo_url: GitHub repository URL
340
+ asset_name: Name of the asset to download
341
+ target_path: Where to save the downloaded asset
342
+
343
+ Returns:
344
+ True if successful, False otherwise
345
+ """
346
+ # TODO: Find release with the asset
347
+ # TODO: Download the asset
348
+ # TODO: Save to target path
349
+ return False
@@ -0,0 +1,230 @@
1
+ """Comfy Registry API client for node discovery, validation, and metadata retrieval."""
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+
8
+ from comfygit_core.caching.api_cache import APICacheManager
9
+ from comfygit_core.constants import DEFAULT_REGISTRY_URL
10
+ from comfygit_core.logging.logging_config import get_logger
11
+ from comfygit_core.models.exceptions import (
12
+ CDNodeNotFoundError,
13
+ CDRegistryAuthError,
14
+ CDRegistryConnectionError,
15
+ CDRegistryError,
16
+ CDRegistryServerError,
17
+ )
18
+ from comfygit_core.models.registry import RegistryNodeInfo, RegistryNodeVersion
19
+ from comfygit_core.utils.retry import (
20
+ RateLimitManager,
21
+ RetryConfig,
22
+ retry_on_rate_limit,
23
+ )
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class ComfyRegistryClient:
29
+ """Client for interacting with the Comfy Registry API.
30
+
31
+ Provides node discovery, validation, version management, and download URLs.
32
+ Includes intelligent fuzzy matching and caching for optimal performance.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ cache_manager: APICacheManager,
38
+ base_url: str = DEFAULT_REGISTRY_URL,
39
+ ):
40
+ self.base_url = base_url
41
+ self.cache_manager = cache_manager
42
+ self.rate_limiter = RateLimitManager(min_interval=0.05)
43
+ self.retry_config = RetryConfig(
44
+ max_retries=3,
45
+ initial_delay=0.5,
46
+ max_delay=30.0,
47
+ exponential_base=2.0,
48
+ jitter=True,
49
+ )
50
+
51
+ def get_node(self, node_id: str) -> RegistryNodeInfo | None:
52
+ """Get exact node by ID from registry.
53
+
54
+ Args:
55
+ node_id: Exact registry node ID
56
+
57
+ Returns:
58
+ NodeInfo if found, None otherwise
59
+
60
+ Raises:
61
+ CDRegistryError: If registry request fails
62
+ """
63
+ url = f"{self.base_url}/nodes/{node_id}"
64
+
65
+ data = self._make_registry_request(url)
66
+
67
+ if data:
68
+ version = data.get('latest_version', {}).get('version', 'unknown')
69
+ logger.debug(f"Found node '{node_id}' in registry (version: {version})")
70
+ return RegistryNodeInfo.from_api_data(data)
71
+
72
+ return None
73
+
74
+ def install_node(
75
+ self, node_id: str, version: str | None
76
+ ) -> RegistryNodeVersion | None:
77
+ """Get the exact node download info by ID from registry.
78
+
79
+ Args:
80
+ node_id: Exact registry node ID
81
+
82
+ Returns:
83
+ NodeInfo if found, None otherwise
84
+
85
+ Raises:
86
+ CDRegistryError: If registry request fails
87
+ """
88
+ url = f"{self.base_url}/nodes/{node_id}/install"
89
+
90
+ if version:
91
+ url += f"?version={version}"
92
+
93
+ data = self._make_registry_request(url)
94
+
95
+ if data:
96
+ version = data.get('version', 'unknown')
97
+ logger.debug(f"Found install info for node '{node_id}' (version: {version})")
98
+ return RegistryNodeVersion.from_api_data(data)
99
+
100
+ return None
101
+
102
+ def search_nodes(
103
+ self, query: str, limit: int = 20
104
+ ) -> list[RegistryNodeInfo] | None:
105
+ """Search for nodes by keyword.
106
+
107
+ Args:
108
+ query: Search term (name, author, description)
109
+ limit: Max results
110
+
111
+ Returns:
112
+ List of matching nodes
113
+
114
+ Raises:
115
+ CDRegistryError: If registry request fails
116
+ """
117
+ params = {"search": query, "limit": limit}
118
+ url = f"{self.base_url}/nodes/search?{urllib.parse.urlencode(params)}"
119
+
120
+ data = self._make_registry_request(url)
121
+
122
+ if data and "nodes" in data:
123
+ return [
124
+ info
125
+ for n in data["nodes"]
126
+ if (info := RegistryNodeInfo.from_api_data(n)) is not None
127
+ ]
128
+
129
+ return None
130
+
131
+ def get_node_requirements(self, node_id: str) -> list[str]:
132
+ """Get requirements for a specific node version.
133
+
134
+ Raises:
135
+ CDNodeNotFoundError: If node doesn't exist
136
+ CDRegistryError: If registry request fails
137
+ """
138
+ url = f"{self.base_url}/nodes/{node_id}/install"
139
+
140
+ install_info = self._make_registry_request(url)
141
+
142
+ if install_info is None:
143
+ # This is more exceptional - we're asking for requirements
144
+ # of a node we expect to exist
145
+ raise CDNodeNotFoundError(f"Node '{node_id}' not found in registry")
146
+
147
+ return install_info.get("dependencies", [])
148
+
149
+ @retry_on_rate_limit(RetryConfig(max_retries=3, initial_delay=0.5, max_delay=30.0))
150
+ def _make_registry_request(self, url: str) -> dict | None:
151
+ """Make a request to Registry API with retry logic.
152
+
153
+ Returns:
154
+ Response data or None for 404 (not found)
155
+
156
+ Raises:
157
+ CDRegistryAuthError: For 401/403 authentication issues
158
+ CDRegistryServerError: For 5xx server errors
159
+ CDRegistryConnectionError: For network issues
160
+ """
161
+
162
+ # Check cache first
163
+ logger.debug(f"Registry client: Checking cache for '{url}'")
164
+ cached_data = self.cache_manager.get("registry", url)
165
+ if cached_data is not None:
166
+ logger.debug(f"Registry client: Using cached data for '{url}'")
167
+ return cached_data
168
+
169
+ # Rate limit ourselves
170
+ self.rate_limiter.wait_if_needed("registry_api")
171
+
172
+ req = urllib.request.Request(url)
173
+ req.add_header("User-Agent", "ComfyDock/1.0")
174
+
175
+ try:
176
+ with urllib.request.urlopen(req, timeout=30) as response:
177
+ if response.status == 200:
178
+ json_data = json.loads(response.read().decode("utf-8"))
179
+ logger.debug(f"Registry client: Got data for '{url}'")
180
+ self.cache_manager.set("registry", url, json_data)
181
+ return json_data
182
+
183
+ except urllib.error.HTTPError as e:
184
+ if e.code == 404:
185
+ # Not found is expected for many operations
186
+ logger.debug(f"Registry: Node not found at '{url}'")
187
+ return None
188
+
189
+ elif e.code in (401, 403):
190
+ # Authentication/authorization errors
191
+ logger.error(f"Registry auth error for '{url}': HTTP {e.code}")
192
+
193
+ # Try to get more details from response
194
+ error_msg = f"Registry authentication failed (HTTP {e.code})"
195
+ try:
196
+ error_data = e.read().decode("utf-8")
197
+ if error_data:
198
+ error_msg += f": {error_data}"
199
+ except:
200
+ pass
201
+
202
+ raise CDRegistryAuthError(error_msg) from e
203
+
204
+ elif e.code >= 500:
205
+ # Server errors - these might be temporary
206
+ logger.error(f"Registry server error for '{url}': HTTP {e.code}")
207
+ raise CDRegistryServerError(
208
+ f"Registry server error (HTTP {e.code})"
209
+ ) from e
210
+
211
+ else:
212
+ # Other HTTP errors
213
+ logger.error(f"Registry HTTP error for '{url}': {e.code} {e.reason}")
214
+ raise CDRegistryError(
215
+ f"Registry request failed: HTTP {e.code} {e.reason}"
216
+ ) from e
217
+
218
+ except urllib.error.URLError as e:
219
+ # Network errors (connection refused, DNS failure, etc.)
220
+ logger.error(f"Registry connection error for '{url}': {e}")
221
+ raise CDRegistryConnectionError(
222
+ f"Failed to connect to registry: {e.reason}"
223
+ ) from e
224
+
225
+ except Exception as e:
226
+ # Unexpected errors
227
+ logger.error(f"Unexpected error accessing registry at '{url}': {e}")
228
+ raise CDRegistryError(f"Registry request failed: {e}") from e
229
+
230
+ return None # Shouldn't reach here, but for completeness