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,569 @@
|
|
|
1
|
+
"""UV project management with smart orchestration and pyproject.toml coordination."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..integrations.uv_command import UVCommand
|
|
9
|
+
from ..logging.logging_config import get_logger
|
|
10
|
+
from ..models.exceptions import CDPyprojectError, UVCommandError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..managers.pyproject_manager import PyprojectManager
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UVProjectManager:
|
|
19
|
+
"""High-level UV project management with smart workflows and pyproject.toml coordination."""
|
|
20
|
+
|
|
21
|
+
# Marker translations for converting requirements.txt to pyproject.toml format
|
|
22
|
+
MARKER_TRANSLATIONS = {
|
|
23
|
+
'platform_system == "Linux"': "sys_platform == 'linux'",
|
|
24
|
+
'platform_system == "Windows"': "sys_platform == 'win32'",
|
|
25
|
+
'platform_system == "Darwin"': "sys_platform == 'darwin'",
|
|
26
|
+
'platform_system': 'sys_platform',
|
|
27
|
+
'platform_machine': 'platform_machine',
|
|
28
|
+
'"Linux"': "'linux'",
|
|
29
|
+
'"Windows"': "'win32'",
|
|
30
|
+
'"Darwin"': "'darwin'",
|
|
31
|
+
'"x86_64"': "'x86_64'",
|
|
32
|
+
'python_version': 'python_version',
|
|
33
|
+
'python_full_version': 'python_full_version',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
uv_command: UVCommand,
|
|
39
|
+
pyproject_manager: PyprojectManager,
|
|
40
|
+
):
|
|
41
|
+
self.uv = uv_command
|
|
42
|
+
self.pyproject = pyproject_manager
|
|
43
|
+
|
|
44
|
+
# ===== Properties =====
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def project_path(self) -> Path:
|
|
48
|
+
return self.pyproject.path.parent
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def python_executable(self) -> Path:
|
|
52
|
+
return self.uv.python_executable
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def binary(self) -> str:
|
|
56
|
+
return self.uv.binary
|
|
57
|
+
|
|
58
|
+
# ===== Basic Operations =====
|
|
59
|
+
|
|
60
|
+
def init_project(self, name: str | None = None, python_version: str | None = None, **flags) -> str:
|
|
61
|
+
result = self.uv.init(name=name, python=python_version, **flags)
|
|
62
|
+
return result.stdout
|
|
63
|
+
|
|
64
|
+
def add_dependency(
|
|
65
|
+
self,
|
|
66
|
+
package: str | None = None,
|
|
67
|
+
packages: list[str] | None = None,
|
|
68
|
+
requirements_file: Path | None = None,
|
|
69
|
+
upgrade: bool = False,
|
|
70
|
+
group: str | None = None,
|
|
71
|
+
dev: bool = False,
|
|
72
|
+
editable: bool = False,
|
|
73
|
+
bounds: str | None = None,
|
|
74
|
+
**flags
|
|
75
|
+
) -> str:
|
|
76
|
+
"""Add one or more dependencies to the project.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
package: Single package to add (legacy parameter)
|
|
80
|
+
packages: List of packages to add
|
|
81
|
+
requirements_file: Path to requirements file
|
|
82
|
+
upgrade: Whether to upgrade existing packages
|
|
83
|
+
group: Dependency group name (e.g., 'optional-cuda')
|
|
84
|
+
dev: Add to dev dependencies
|
|
85
|
+
editable: Install as editable (for local development)
|
|
86
|
+
bounds: Version specifier style ('lower', 'major', 'minor', 'exact')
|
|
87
|
+
**flags: Additional UV flags
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
UV command stdout
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: If none of package, packages, or requirements_file is provided
|
|
94
|
+
"""
|
|
95
|
+
if packages:
|
|
96
|
+
pkg_list = packages
|
|
97
|
+
elif package:
|
|
98
|
+
pkg_list = [package]
|
|
99
|
+
elif requirements_file:
|
|
100
|
+
pkg_list = None
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError("Either 'package', 'packages', or 'requirements_file' must be provided")
|
|
103
|
+
|
|
104
|
+
result = self.uv.add(
|
|
105
|
+
packages=pkg_list,
|
|
106
|
+
requirements_file=requirements_file,
|
|
107
|
+
upgrade=upgrade,
|
|
108
|
+
group=group,
|
|
109
|
+
dev=dev,
|
|
110
|
+
editable=editable,
|
|
111
|
+
bounds=bounds,
|
|
112
|
+
**flags
|
|
113
|
+
)
|
|
114
|
+
return result.stdout
|
|
115
|
+
|
|
116
|
+
def remove_dependency(self, package: str | None = None, packages: list[str] | None = None, **flags) -> dict:
|
|
117
|
+
"""Remove one or more dependencies from the project.
|
|
118
|
+
|
|
119
|
+
Filters out packages that don't exist in dependencies before calling uv remove.
|
|
120
|
+
This makes the operation idempotent and safe to call with non-existent packages.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
package: Single package to remove (legacy parameter)
|
|
124
|
+
packages: List of packages to remove
|
|
125
|
+
**flags: Additional UV flags
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dict with 'removed' (list of packages removed) and 'skipped' (list of packages not in deps)
|
|
129
|
+
"""
|
|
130
|
+
if packages:
|
|
131
|
+
pkg_list = packages
|
|
132
|
+
elif package:
|
|
133
|
+
pkg_list = [package]
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError("Either 'package' or 'packages' must be provided")
|
|
136
|
+
|
|
137
|
+
# Get current dependencies to filter what actually exists
|
|
138
|
+
from ..utils.dependency_parser import parse_dependency_string
|
|
139
|
+
config = self.pyproject.load()
|
|
140
|
+
current_deps = config.get('project', {}).get('dependencies', [])
|
|
141
|
+
current_pkg_names = {parse_dependency_string(dep)[0].lower() for dep in current_deps}
|
|
142
|
+
|
|
143
|
+
# Filter to only packages that exist
|
|
144
|
+
existing_packages = [pkg for pkg in pkg_list if pkg.lower() in current_pkg_names]
|
|
145
|
+
missing_packages = [pkg for pkg in pkg_list if pkg.lower() not in current_pkg_names]
|
|
146
|
+
|
|
147
|
+
# If nothing to remove, return early
|
|
148
|
+
if not existing_packages:
|
|
149
|
+
return {
|
|
150
|
+
'removed': [],
|
|
151
|
+
'skipped': missing_packages
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Remove only existing packages
|
|
155
|
+
result = self.uv.remove(existing_packages, **flags)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
'removed': existing_packages,
|
|
159
|
+
'skipped': missing_packages
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
def sync_project(self, verbose: bool = False, **flags) -> str:
|
|
163
|
+
result = self.uv.sync(verbose=verbose, **flags)
|
|
164
|
+
return result.stdout
|
|
165
|
+
|
|
166
|
+
def lock_project(self, **flags) -> str:
|
|
167
|
+
result = self.uv.lock(**flags)
|
|
168
|
+
return result.stdout
|
|
169
|
+
|
|
170
|
+
def run_command(self, command: list[str], **flags) -> str:
|
|
171
|
+
result = self.uv.run(command, **flags)
|
|
172
|
+
return result.stdout
|
|
173
|
+
|
|
174
|
+
def create_venv(self, venv_path: Path, python_version: str | None = None, **flags) -> str:
|
|
175
|
+
result = self.uv.venv(venv_path, python=python_version, **flags)
|
|
176
|
+
return result.stdout
|
|
177
|
+
|
|
178
|
+
# ===== Smart Requirements Handling =====
|
|
179
|
+
|
|
180
|
+
def add_requirements_with_sources(
|
|
181
|
+
self,
|
|
182
|
+
requirements: Path | list[str],
|
|
183
|
+
group: str | None = None,
|
|
184
|
+
**flags
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Smart requirements.txt handler that coordinates UV and pyproject.toml."""
|
|
187
|
+
logger.info("Adding requirements with sources...")
|
|
188
|
+
|
|
189
|
+
categorized = self._categorize_requirements(requirements)
|
|
190
|
+
|
|
191
|
+
# Create temp file with everything EXCEPT multi-URL packages
|
|
192
|
+
tmp_path = None
|
|
193
|
+
try:
|
|
194
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp:
|
|
195
|
+
for line in categorized["regular_lines"]:
|
|
196
|
+
tmp.write(line + "\n")
|
|
197
|
+
for line in categorized["git_urls"]:
|
|
198
|
+
tmp.write(line + "\n")
|
|
199
|
+
for line in categorized["single_urls"]:
|
|
200
|
+
tmp.write(line + "\n")
|
|
201
|
+
tmp_path = Path(tmp.name)
|
|
202
|
+
|
|
203
|
+
# Let UV handle everything it can
|
|
204
|
+
if tmp_path.stat().st_size > 0:
|
|
205
|
+
self.add_dependency(requirements_file=tmp_path, group=group, **flags)
|
|
206
|
+
|
|
207
|
+
# Post-process multi-URL packages via pyproject.toml
|
|
208
|
+
for package, urls_with_markers in categorized["multi_url_packages"].items():
|
|
209
|
+
self._add_url_sources_with_markers(package, urls_with_markers, group)
|
|
210
|
+
|
|
211
|
+
logger.info("Successfully added all requirements")
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise UVCommandError(f"Failed to add requirements: {e}")
|
|
214
|
+
finally:
|
|
215
|
+
if tmp_path and tmp_path.exists():
|
|
216
|
+
tmp_path.unlink()
|
|
217
|
+
|
|
218
|
+
def _categorize_requirements(self, requirements: Path | list[str]) -> dict:
|
|
219
|
+
"""Categorize requirements into UV-compatible and special handling categories."""
|
|
220
|
+
logger.info("Categorizing requirements...")
|
|
221
|
+
|
|
222
|
+
categorized = {
|
|
223
|
+
'regular_lines': [],
|
|
224
|
+
'git_urls': [],
|
|
225
|
+
'single_urls': [],
|
|
226
|
+
'multi_url_packages': {}, # package_name -> [{"url": ..., "marker": ...}, ...]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
url_packages = {} # Track URL-based packages to detect multiples
|
|
230
|
+
|
|
231
|
+
# Handle both Path and list[str] input
|
|
232
|
+
if isinstance(requirements, Path):
|
|
233
|
+
with open(requirements, encoding='utf-8') as f:
|
|
234
|
+
lines = f.readlines()
|
|
235
|
+
else:
|
|
236
|
+
lines = requirements
|
|
237
|
+
|
|
238
|
+
for line in lines:
|
|
239
|
+
line = line.strip()
|
|
240
|
+
if not line or line.startswith('#'):
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Git URLs - UV handles these perfectly
|
|
244
|
+
if line.startswith('git+') or 'git+' in line:
|
|
245
|
+
categorized['git_urls'].append(line)
|
|
246
|
+
|
|
247
|
+
# Direct URLs (http/https/file)
|
|
248
|
+
elif any(proto in line for proto in ['http://', 'https://', 'file://']):
|
|
249
|
+
package_name, url, marker = self._parse_url_requirement(line)
|
|
250
|
+
|
|
251
|
+
if package_name in url_packages:
|
|
252
|
+
# Multiple URLs for same package - needs special handling
|
|
253
|
+
if package_name not in categorized['multi_url_packages']:
|
|
254
|
+
# Move the first occurrence from single_urls to multi_url
|
|
255
|
+
categorized['multi_url_packages'][package_name] = [url_packages[package_name]]
|
|
256
|
+
# Remove from single_urls
|
|
257
|
+
categorized['single_urls'] = [
|
|
258
|
+
line for line in categorized['single_urls']
|
|
259
|
+
if url_packages[package_name]['url'] not in line
|
|
260
|
+
]
|
|
261
|
+
categorized['multi_url_packages'][package_name].append({
|
|
262
|
+
'url': url,
|
|
263
|
+
'marker': marker
|
|
264
|
+
})
|
|
265
|
+
else:
|
|
266
|
+
# First URL for this package
|
|
267
|
+
url_packages[package_name] = {'url': url, 'marker': marker}
|
|
268
|
+
categorized['single_urls'].append(line)
|
|
269
|
+
|
|
270
|
+
# Regular package specifications
|
|
271
|
+
else:
|
|
272
|
+
categorized['regular_lines'].append(line)
|
|
273
|
+
|
|
274
|
+
return categorized
|
|
275
|
+
|
|
276
|
+
def _parse_url_requirement(self, line: str) -> tuple[str, str, str | None]:
|
|
277
|
+
"""Parse a URL requirement line. Returns (package_name, url, marker)."""
|
|
278
|
+
marker = None
|
|
279
|
+
|
|
280
|
+
# Check for environment marker
|
|
281
|
+
if ';' in line:
|
|
282
|
+
url_part, marker_part = line.split(';', 1)
|
|
283
|
+
marker = self._translate_marker(marker_part.strip())
|
|
284
|
+
else:
|
|
285
|
+
url_part = line
|
|
286
|
+
|
|
287
|
+
# Handle "package @ url" format
|
|
288
|
+
if ' @ ' in url_part:
|
|
289
|
+
package_name, url = url_part.split(' @ ', 1)
|
|
290
|
+
package_name = package_name.strip()
|
|
291
|
+
url = url.strip()
|
|
292
|
+
else:
|
|
293
|
+
# Extract from URL
|
|
294
|
+
url = url_part.strip()
|
|
295
|
+
package_name = self._extract_package_from_url(url)
|
|
296
|
+
|
|
297
|
+
return package_name, url, marker
|
|
298
|
+
|
|
299
|
+
def _extract_package_from_url(self, url: str) -> str:
|
|
300
|
+
"""Extract package name from various URL types."""
|
|
301
|
+
# Remove URL parameters
|
|
302
|
+
url = url.split('?')[0].split('#')[0]
|
|
303
|
+
|
|
304
|
+
# Get filename
|
|
305
|
+
filename = url.split('/')[-1]
|
|
306
|
+
|
|
307
|
+
if filename.endswith('.whl'):
|
|
308
|
+
# Wheel format: {distribution}-{version}[-{build tag}]-{python}-{abi}-{platform}.whl
|
|
309
|
+
parts = filename[:-4].split('-') # Remove .whl
|
|
310
|
+
package_name = parts[0]
|
|
311
|
+
elif filename.endswith(('.tar.gz', '.zip', '.tar.bz2')):
|
|
312
|
+
# Source dist format: {name}-{version}.tar.gz
|
|
313
|
+
if filename.endswith('.tar.gz'):
|
|
314
|
+
base = filename[:-7]
|
|
315
|
+
elif filename.endswith('.tar.bz2'):
|
|
316
|
+
base = filename[:-8]
|
|
317
|
+
else:
|
|
318
|
+
base = filename[:-4]
|
|
319
|
+
# Split on last hyphen to separate name from version
|
|
320
|
+
parts = base.rsplit('-', 1)
|
|
321
|
+
package_name = parts[0] if parts else base
|
|
322
|
+
else:
|
|
323
|
+
# Fallback: use filename without extension
|
|
324
|
+
package_name = filename.split('.')[0]
|
|
325
|
+
|
|
326
|
+
# Normalize: convert underscores to hyphens (PEP 503)
|
|
327
|
+
return package_name.replace('_', '-').lower()
|
|
328
|
+
|
|
329
|
+
def _translate_marker(self, marker: str) -> str:
|
|
330
|
+
"""Translate requirements.txt markers to pyproject.toml format."""
|
|
331
|
+
result = marker
|
|
332
|
+
|
|
333
|
+
# Apply translations from class constant
|
|
334
|
+
for old, new in self.MARKER_TRANSLATIONS.items():
|
|
335
|
+
result = result.replace(old, new)
|
|
336
|
+
|
|
337
|
+
# Normalize all remaining double quotes to single quotes
|
|
338
|
+
result = result.replace('"', "'")
|
|
339
|
+
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
def _add_url_sources_with_markers(self, package_name: str, urls_with_markers: list[dict],
|
|
343
|
+
group: str | None = None) -> None:
|
|
344
|
+
"""Update [tool.uv.sources] and optionally add to dependency group."""
|
|
345
|
+
self.pyproject.uv_config.add_url_sources(package_name, urls_with_markers, group)
|
|
346
|
+
logger.info(f"Added {len(urls_with_markers)} URL source(s) for '{package_name}'")
|
|
347
|
+
|
|
348
|
+
# ===== Constraint and Index Management =====
|
|
349
|
+
|
|
350
|
+
def add_constraint_dependency(self, package: str) -> None:
|
|
351
|
+
"""Add a constraint dependency to the project's pyproject.toml."""
|
|
352
|
+
self.pyproject.uv_config.add_constraint(package)
|
|
353
|
+
logger.info(f"Added constraint: {package}")
|
|
354
|
+
|
|
355
|
+
def create_index(self, name: str, url: str, explicit: bool = True) -> None:
|
|
356
|
+
"""Create a new index in the project's pyproject.toml."""
|
|
357
|
+
self.pyproject.uv_config.add_index(name, url, explicit)
|
|
358
|
+
logger.info(f"Created index '{name}'")
|
|
359
|
+
|
|
360
|
+
def add_source_index(self, package_name: str, index: str) -> None:
|
|
361
|
+
"""Add a source index mapping for a package in pyproject.toml."""
|
|
362
|
+
# Validate that the index exists
|
|
363
|
+
indexes = self.pyproject.uv_config.get_indexes()
|
|
364
|
+
if not any(idx.get('name') == index for idx in indexes):
|
|
365
|
+
raise CDPyprojectError(f"Index '{index}' does not exist. Please create it first using create_index()")
|
|
366
|
+
|
|
367
|
+
self.pyproject.uv_config.add_source(package_name, {'index': index})
|
|
368
|
+
logger.info(f"Added source for '{package_name}': index = '{index}'")
|
|
369
|
+
|
|
370
|
+
def remove_dependency_group(self, group_name: str) -> None:
|
|
371
|
+
"""Remove a dependency group from pyproject.toml."""
|
|
372
|
+
self.pyproject.dependencies.remove_group(group_name)
|
|
373
|
+
logger.info(f"Removed dependency group: {group_name}")
|
|
374
|
+
|
|
375
|
+
# ===== Package Operations =====
|
|
376
|
+
|
|
377
|
+
def install_packages(self, packages: list[str] | None = None, requirements_file: Path | None = None,
|
|
378
|
+
python: Path | None = None, torch_backend: str | None = None,
|
|
379
|
+
verbose: bool = False, **flags) -> str:
|
|
380
|
+
"""Install packages using uv pip install."""
|
|
381
|
+
result = self.uv.pip_install(
|
|
382
|
+
packages=packages,
|
|
383
|
+
requirements_file=requirements_file,
|
|
384
|
+
python=python,
|
|
385
|
+
torch_backend=torch_backend,
|
|
386
|
+
verbose=verbose,
|
|
387
|
+
**flags
|
|
388
|
+
)
|
|
389
|
+
return result.stdout
|
|
390
|
+
|
|
391
|
+
def show_package(self, package: str, python: Path) -> str:
|
|
392
|
+
"""Show package information."""
|
|
393
|
+
result = self.uv.pip_show(package, python)
|
|
394
|
+
return result.stdout
|
|
395
|
+
|
|
396
|
+
def list_packages(self, python: Path) -> str:
|
|
397
|
+
"""List installed packages."""
|
|
398
|
+
result = self.uv.pip_list(python)
|
|
399
|
+
return result.stdout
|
|
400
|
+
|
|
401
|
+
def uninstall_packages(self, packages: list[str], python: Path) -> str:
|
|
402
|
+
"""Uninstall packages."""
|
|
403
|
+
result = self.uv.pip_install(packages=[f"-{pkg}" for pkg in packages], python=python)
|
|
404
|
+
return result.stdout
|
|
405
|
+
|
|
406
|
+
def freeze_packages(self, python: Path) -> str:
|
|
407
|
+
"""Export installed packages in requirements format."""
|
|
408
|
+
result = self.uv.pip_freeze(python)
|
|
409
|
+
return result.stdout
|
|
410
|
+
|
|
411
|
+
def pip_compile(self, in_requirements_file: Path | None = None,
|
|
412
|
+
out_requirements_file: Path | None = None, **flags) -> str:
|
|
413
|
+
"""Compile dependencies to requirements format."""
|
|
414
|
+
# Check if we're compiling a requirements file or a pyproject.toml
|
|
415
|
+
if in_requirements_file and in_requirements_file.exists():
|
|
416
|
+
input_file = in_requirements_file
|
|
417
|
+
else:
|
|
418
|
+
input_file = self.pyproject.path
|
|
419
|
+
|
|
420
|
+
# Add dependency groups if compiling pyproject.toml
|
|
421
|
+
if self.pyproject.exists():
|
|
422
|
+
try:
|
|
423
|
+
dependency_groups = self.pyproject.dependencies.get_groups()
|
|
424
|
+
for group_name in dependency_groups.keys():
|
|
425
|
+
flags.setdefault('group', []).append(group_name) if 'group' in flags else None
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.debug(f"Could not get dependency groups: {e}")
|
|
428
|
+
|
|
429
|
+
result = self.uv.pip_compile(
|
|
430
|
+
input_file=input_file,
|
|
431
|
+
output_file=out_requirements_file,
|
|
432
|
+
**flags
|
|
433
|
+
)
|
|
434
|
+
return result.stdout
|
|
435
|
+
|
|
436
|
+
# ===== Tool Management =====
|
|
437
|
+
|
|
438
|
+
def run_tool(self, tool: str, args: list[str] | None = None) -> str:
|
|
439
|
+
"""Run a tool in an isolated environment using uvx."""
|
|
440
|
+
result = self.uv.tool_run(tool, args)
|
|
441
|
+
return result.stdout
|
|
442
|
+
|
|
443
|
+
def install_tool(self, tool: str) -> str:
|
|
444
|
+
"""Install a tool globally."""
|
|
445
|
+
result = self.uv.tool_install(tool)
|
|
446
|
+
return result.stdout
|
|
447
|
+
|
|
448
|
+
# ===== Python Management =====
|
|
449
|
+
|
|
450
|
+
def install_python(self, version: str) -> str:
|
|
451
|
+
"""Install a Python version."""
|
|
452
|
+
result = self.uv.python_install(version)
|
|
453
|
+
return result.stdout
|
|
454
|
+
|
|
455
|
+
def list_python_versions(self) -> str:
|
|
456
|
+
"""List available Python versions."""
|
|
457
|
+
result = self.uv.python_list()
|
|
458
|
+
return result.stdout
|
|
459
|
+
|
|
460
|
+
# ===== Advanced Sync Operations =====
|
|
461
|
+
|
|
462
|
+
def sync_dependencies_progressive(
|
|
463
|
+
self,
|
|
464
|
+
dry_run: bool = False,
|
|
465
|
+
callbacks = None,
|
|
466
|
+
verbose: bool = False
|
|
467
|
+
) -> dict:
|
|
468
|
+
"""Install dependencies progressively with graceful optional group handling.
|
|
469
|
+
|
|
470
|
+
Installs dependencies in phases:
|
|
471
|
+
1. Base dependencies + all groups together with iterative optional group removal on failure
|
|
472
|
+
|
|
473
|
+
If optional groups fail to build, we iteratively:
|
|
474
|
+
- Parse the error to identify the failing group
|
|
475
|
+
- Remove that group from pyproject.toml
|
|
476
|
+
- Delete uv.lock to force re-resolution
|
|
477
|
+
- Retry the sync with all remaining groups
|
|
478
|
+
- Continue until success or max retries
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
dry_run: If True, don't actually install
|
|
482
|
+
callbacks: Optional callbacks for progress reporting
|
|
483
|
+
verbose: If True, show uv output in real-time
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Dict with keys:
|
|
487
|
+
- packages_synced: bool
|
|
488
|
+
- dependency_groups_installed: list[str]
|
|
489
|
+
- dependency_groups_failed: list[tuple[str, str]]
|
|
490
|
+
"""
|
|
491
|
+
from ..constants import MAX_OPT_GROUP_RETRIES
|
|
492
|
+
from ..utils.uv_error_handler import parse_failed_dependency_group
|
|
493
|
+
|
|
494
|
+
result = {
|
|
495
|
+
"packages_synced": False,
|
|
496
|
+
"dependency_groups_installed": [],
|
|
497
|
+
"dependency_groups_failed": []
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
attempts = 0
|
|
501
|
+
|
|
502
|
+
logger.info("Installing dependencies with all groups...")
|
|
503
|
+
|
|
504
|
+
while attempts < MAX_OPT_GROUP_RETRIES:
|
|
505
|
+
try:
|
|
506
|
+
# Get all dependency groups (may have changed after removal)
|
|
507
|
+
dep_groups = self.pyproject.dependencies.get_groups()
|
|
508
|
+
|
|
509
|
+
if dep_groups:
|
|
510
|
+
# Install base + all groups together
|
|
511
|
+
group_list = list(dep_groups.keys())
|
|
512
|
+
logger.debug(f"Syncing with groups: {group_list}")
|
|
513
|
+
self.sync_project(group=group_list, dry_run=dry_run, verbose=verbose)
|
|
514
|
+
|
|
515
|
+
# Track successful installations
|
|
516
|
+
result["dependency_groups_installed"].extend(group_list)
|
|
517
|
+
else:
|
|
518
|
+
# No groups - just sync base dependencies
|
|
519
|
+
logger.debug("No dependency groups, syncing base only")
|
|
520
|
+
self.sync_project(dry_run=dry_run, no_default_groups=True, verbose=verbose)
|
|
521
|
+
|
|
522
|
+
result["packages_synced"] = True
|
|
523
|
+
break # Success - exit loop
|
|
524
|
+
|
|
525
|
+
except UVCommandError as e:
|
|
526
|
+
failed_group = parse_failed_dependency_group(e.stderr or "")
|
|
527
|
+
|
|
528
|
+
if failed_group and failed_group.startswith('optional-'):
|
|
529
|
+
attempts += 1
|
|
530
|
+
logger.warning(
|
|
531
|
+
f"Build failed for optional group '{failed_group}' (attempt {attempts}/{MAX_OPT_GROUP_RETRIES}), "
|
|
532
|
+
"removing and retrying..."
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Remove the problematic group
|
|
536
|
+
try:
|
|
537
|
+
self.pyproject.dependencies.remove_group(failed_group)
|
|
538
|
+
except ValueError:
|
|
539
|
+
pass # Group already gone
|
|
540
|
+
|
|
541
|
+
# Delete lockfile to force re-resolution
|
|
542
|
+
lockfile = self.project_path / "uv.lock"
|
|
543
|
+
if lockfile.exists():
|
|
544
|
+
lockfile.unlink()
|
|
545
|
+
logger.debug("Deleted uv.lock to force re-resolution")
|
|
546
|
+
|
|
547
|
+
result["dependency_groups_failed"].append((failed_group, "Build failed (incompatible platform)"))
|
|
548
|
+
|
|
549
|
+
if callbacks:
|
|
550
|
+
callbacks.on_dependency_group_complete(failed_group, success=False, error="Build failed - removed")
|
|
551
|
+
|
|
552
|
+
if attempts >= MAX_OPT_GROUP_RETRIES:
|
|
553
|
+
raise RuntimeError(
|
|
554
|
+
f"Failed to install dependencies after {MAX_OPT_GROUP_RETRIES} attempts. "
|
|
555
|
+
f"Removed groups: {[g for g, _ in result['dependency_groups_failed']]}"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# Loop continues for retry
|
|
559
|
+
else:
|
|
560
|
+
# Not an optional group failure - fail immediately
|
|
561
|
+
raise
|
|
562
|
+
|
|
563
|
+
return result
|
|
564
|
+
|
|
565
|
+
# ===== Utility =====
|
|
566
|
+
|
|
567
|
+
def version(self) -> str:
|
|
568
|
+
"""Get the installed UV version."""
|
|
569
|
+
return self.uv.version()
|