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