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,111 @@
|
|
|
1
|
+
"""Cross-platform environment directory cleanup utilities."""
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import stat
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..logging.logging_config import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
# Marker file indicating environment creation completed successfully
|
|
14
|
+
COMPLETION_MARKER = ".complete"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _handle_remove_readonly(func, path, exc_info):
|
|
18
|
+
"""Handle Windows readonly/locked files during deletion.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
func: Function that failed (e.g., os.unlink)
|
|
22
|
+
path: Path that couldn't be removed
|
|
23
|
+
exc_info: Exception info tuple
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
os.chmod(path, stat.S_IWRITE)
|
|
27
|
+
time.sleep(0.05) # Brief delay for handle release
|
|
28
|
+
func(path)
|
|
29
|
+
except Exception:
|
|
30
|
+
pass # Let rmtree handle final error
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def remove_environment_directory(env_path: Path) -> None:
|
|
34
|
+
"""Remove environment directory with platform-specific handling.
|
|
35
|
+
|
|
36
|
+
This handles Windows file locks and permission issues that commonly
|
|
37
|
+
occur when Python processes or uv operations are interrupted.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
env_path: Path to environment directory
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
PermissionError: If deletion fails due to permissions
|
|
44
|
+
OSError: If deletion fails for other reasons
|
|
45
|
+
"""
|
|
46
|
+
if not env_path.exists():
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
if platform.system() == "Windows":
|
|
51
|
+
shutil.rmtree(env_path, onexc=_handle_remove_readonly)
|
|
52
|
+
else:
|
|
53
|
+
shutil.rmtree(env_path)
|
|
54
|
+
logger.debug(f"Removed environment directory: {env_path}")
|
|
55
|
+
except PermissionError as e:
|
|
56
|
+
raise PermissionError(
|
|
57
|
+
f"Cannot delete '{env_path.name}': files may be in use. "
|
|
58
|
+
f"Try closing applications using this environment."
|
|
59
|
+
) from e
|
|
60
|
+
except OSError as e:
|
|
61
|
+
raise OSError(f"Failed to delete environment '{env_path.name}': {e}") from e
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cleanup_partial_environment(env_path: Path) -> bool:
|
|
65
|
+
"""Clean up partial environment after creation failure.
|
|
66
|
+
|
|
67
|
+
Uses platform-specific cleanup and provides user feedback on failure.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
env_path: Path to partial environment directory
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if cleanup succeeded, False if manual intervention needed
|
|
74
|
+
"""
|
|
75
|
+
if not env_path.exists():
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
logger.debug(f"Cleaning up partial environment at {env_path}")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
remove_environment_directory(env_path)
|
|
82
|
+
return True
|
|
83
|
+
except (PermissionError, OSError) as e:
|
|
84
|
+
logger.warning(f"Failed to clean up partial environment: {e}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def mark_environment_complete(cec_path: Path) -> None:
|
|
89
|
+
"""Mark environment as fully initialized.
|
|
90
|
+
|
|
91
|
+
Creates a completion marker file that list_environments() uses to
|
|
92
|
+
filter out partial/broken environments.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
cec_path: Path to .cec directory
|
|
96
|
+
"""
|
|
97
|
+
marker_file = cec_path / COMPLETION_MARKER
|
|
98
|
+
marker_file.touch()
|
|
99
|
+
logger.debug(f"Marked environment as complete: {marker_file}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def is_environment_complete(cec_path: Path) -> bool:
|
|
103
|
+
"""Check if environment was fully initialized.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
cec_path: Path to .cec directory
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if environment has completion marker
|
|
110
|
+
"""
|
|
111
|
+
return (cec_path / COMPLETION_MARKER).exists()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Filesystem manipulation utilities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..logging.logging_config import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def flatten_if_single_dir(path: Path) -> bool:
|
|
13
|
+
"""Flatten directory structure if it contains a single nested directory.
|
|
14
|
+
|
|
15
|
+
Some archives contain: archive.tar.gz/NodeName-v1.0/*
|
|
16
|
+
We want to flatten to just: NodeName/*
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
path: Path to check and potentially flatten
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
True if flattened, False otherwise
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
contents = list(path.iterdir())
|
|
26
|
+
|
|
27
|
+
# If there's exactly one directory and no files
|
|
28
|
+
if len(contents) == 1 and contents[0].is_dir():
|
|
29
|
+
single_dir = contents[0]
|
|
30
|
+
|
|
31
|
+
logger.info(f"Flattening nested directory: {single_dir.name}")
|
|
32
|
+
|
|
33
|
+
# Move all contents up one level
|
|
34
|
+
for item in single_dir.iterdir():
|
|
35
|
+
dest = path / item.name
|
|
36
|
+
# Handle conflicts by removing destination first
|
|
37
|
+
if dest.exists():
|
|
38
|
+
if dest.is_dir():
|
|
39
|
+
shutil.rmtree(dest)
|
|
40
|
+
else:
|
|
41
|
+
dest.unlink()
|
|
42
|
+
shutil.move(str(item), str(dest))
|
|
43
|
+
|
|
44
|
+
# Remove the now-empty nested directory
|
|
45
|
+
single_dir.rmdir()
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.warning(f"Could not flatten directory: {e}")
|
|
50
|
+
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def ensure_clean_directory(path: Path) -> None:
|
|
55
|
+
"""Ensure a directory exists and is empty.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
path: Directory path to prepare
|
|
59
|
+
"""
|
|
60
|
+
if path.exists():
|
|
61
|
+
if path.is_dir():
|
|
62
|
+
shutil.rmtree(path)
|
|
63
|
+
else:
|
|
64
|
+
path.unlink()
|
|
65
|
+
|
|
66
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def safe_copy_tree(src: Path, dest: Path) -> bool:
|
|
70
|
+
"""Safely copy a directory tree, handling existing destinations.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
src: Source directory
|
|
74
|
+
dest: Destination directory
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if successful, False otherwise
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
# Remove destination if it exists
|
|
81
|
+
if dest.exists():
|
|
82
|
+
if dest.is_dir():
|
|
83
|
+
shutil.rmtree(dest)
|
|
84
|
+
else:
|
|
85
|
+
dest.unlink()
|
|
86
|
+
|
|
87
|
+
# Ensure parent directory exists
|
|
88
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
# Copy the tree
|
|
91
|
+
shutil.copytree(src, dest)
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Failed to copy {src} to {dest}: {e}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_directory_size(path: Path) -> int:
|
|
100
|
+
"""Get total size of a directory in bytes.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
path: Directory path
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Total size in bytes
|
|
107
|
+
"""
|
|
108
|
+
if not path.exists() or not path.is_dir():
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
total_size = 0
|
|
112
|
+
for item in path.rglob("*"):
|
|
113
|
+
if item.is_file():
|
|
114
|
+
total_size += item.stat().st_size
|
|
115
|
+
|
|
116
|
+
return total_size
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def find_file_in_tree(root: Path, filename: str) -> Path | None:
|
|
120
|
+
"""Find a file anywhere in a directory tree.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
root: Root directory to search
|
|
124
|
+
filename: Name of file to find
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Path to first matching file or None
|
|
128
|
+
"""
|
|
129
|
+
for path in root.rglob(filename):
|
|
130
|
+
if path.is_file():
|
|
131
|
+
return path
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def calculate_directory_size(path: Path) -> int:
|
|
136
|
+
"""Calculate total size of a directory tree in bytes.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
path: Directory path to calculate size for
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Total size in bytes
|
|
143
|
+
"""
|
|
144
|
+
total_size = 0
|
|
145
|
+
try:
|
|
146
|
+
for dirpath, dirnames, filenames in os.walk(path):
|
|
147
|
+
for filename in filenames:
|
|
148
|
+
filepath = Path(dirpath) / filename
|
|
149
|
+
if filepath.exists():
|
|
150
|
+
total_size += filepath.stat().st_size
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.warning(f"Could not calculate directory size: {e}")
|
|
153
|
+
|
|
154
|
+
return total_size
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_venv_python(env_path: Path) -> Path | None:
|
|
158
|
+
"""Get the Python executable path for a virtual environment.
|
|
159
|
+
|
|
160
|
+
Cross-platform detection of Python executable in venv.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
env_path: Path to the environment directory
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Path to Python executable or None if not found
|
|
167
|
+
"""
|
|
168
|
+
# Try Unix/Linux/Mac path first
|
|
169
|
+
venv_python = env_path / ".venv" / "bin" / "python"
|
|
170
|
+
if venv_python.exists():
|
|
171
|
+
return venv_python
|
|
172
|
+
|
|
173
|
+
# Try Windows path
|
|
174
|
+
venv_python = env_path / ".venv" / "Scripts" / "python.exe"
|
|
175
|
+
if venv_python.exists():
|
|
176
|
+
return venv_python
|
|
177
|
+
|
|
178
|
+
return None
|