hatch-xclam 0.7.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.
- hatch/__init__.py +21 -0
- hatch/cli_hatch.py +2748 -0
- hatch/environment_manager.py +1375 -0
- hatch/installers/__init__.py +25 -0
- hatch/installers/dependency_installation_orchestrator.py +636 -0
- hatch/installers/docker_installer.py +545 -0
- hatch/installers/hatch_installer.py +198 -0
- hatch/installers/installation_context.py +109 -0
- hatch/installers/installer_base.py +195 -0
- hatch/installers/python_installer.py +342 -0
- hatch/installers/registry.py +179 -0
- hatch/installers/system_installer.py +588 -0
- hatch/mcp_host_config/__init__.py +38 -0
- hatch/mcp_host_config/backup.py +458 -0
- hatch/mcp_host_config/host_management.py +572 -0
- hatch/mcp_host_config/models.py +602 -0
- hatch/mcp_host_config/reporting.py +181 -0
- hatch/mcp_host_config/strategies.py +513 -0
- hatch/package_loader.py +263 -0
- hatch/python_environment_manager.py +734 -0
- hatch/registry_explorer.py +171 -0
- hatch/registry_retriever.py +335 -0
- hatch/template_generator.py +179 -0
- hatch_xclam-0.7.0.dist-info/METADATA +150 -0
- hatch_xclam-0.7.0.dist-info/RECORD +93 -0
- hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
- hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
- hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
- hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/run_environment_tests.py +124 -0
- tests/test_cli_version.py +122 -0
- tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
- tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
- tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
- tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
- tests/test_data_utils.py +472 -0
- tests/test_dependency_orchestrator_consent.py +266 -0
- tests/test_docker_installer.py +524 -0
- tests/test_env_manip.py +991 -0
- tests/test_hatch_installer.py +179 -0
- tests/test_installer_base.py +221 -0
- tests/test_mcp_atomic_operations.py +276 -0
- tests/test_mcp_backup_integration.py +308 -0
- tests/test_mcp_cli_all_host_specific_args.py +303 -0
- tests/test_mcp_cli_backup_management.py +295 -0
- tests/test_mcp_cli_direct_management.py +453 -0
- tests/test_mcp_cli_discovery_listing.py +582 -0
- tests/test_mcp_cli_host_config_integration.py +823 -0
- tests/test_mcp_cli_package_management.py +360 -0
- tests/test_mcp_cli_partial_updates.py +859 -0
- tests/test_mcp_environment_integration.py +520 -0
- tests/test_mcp_host_config_backup.py +257 -0
- tests/test_mcp_host_configuration_manager.py +331 -0
- tests/test_mcp_host_registry_decorator.py +348 -0
- tests/test_mcp_pydantic_architecture_v4.py +603 -0
- tests/test_mcp_server_config_models.py +242 -0
- tests/test_mcp_server_config_type_field.py +221 -0
- tests/test_mcp_sync_functionality.py +316 -0
- tests/test_mcp_user_feedback_reporting.py +359 -0
- tests/test_non_tty_integration.py +281 -0
- tests/test_online_package_loader.py +202 -0
- tests/test_python_environment_manager.py +882 -0
- tests/test_python_installer.py +327 -0
- tests/test_registry.py +51 -0
- tests/test_registry_retriever.py +250 -0
- tests/test_system_installer.py +733 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Installer framework for Hatch dependency management.
|
|
2
|
+
|
|
3
|
+
This package provides a robust, extensible installer interface and concrete
|
|
4
|
+
implementations for different dependency types including Hatch packages,
|
|
5
|
+
Python packages, system packages, and Docker containers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from hatch.installers.installer_base import DependencyInstaller, InstallationError, InstallationContext
|
|
9
|
+
from hatch.installers.hatch_installer import HatchInstaller
|
|
10
|
+
from hatch.installers.python_installer import PythonInstaller
|
|
11
|
+
from hatch.installers.system_installer import SystemInstaller
|
|
12
|
+
from hatch.installers.docker_installer import DockerInstaller
|
|
13
|
+
from hatch.installers.registry import InstallerRegistry, installer_registry
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"DependencyInstaller",
|
|
17
|
+
"InstallationError",
|
|
18
|
+
"InstallationContext",
|
|
19
|
+
#"HatchInstaller", # Not necessary to expose directly, the registry will handle it
|
|
20
|
+
#"PythonInstaller", # Not necessary to expose directly, the registry will handle it
|
|
21
|
+
#"SystemInstaller", # Not necessary to expose directly, the registry will handle it
|
|
22
|
+
#"DockerInstaller", # Not necessary to expose directly, the registry will handle it
|
|
23
|
+
"InstallerRegistry",
|
|
24
|
+
"installer_registry"
|
|
25
|
+
]
|
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"""Dependency installation orchestrator for coordinating package installation.
|
|
2
|
+
|
|
3
|
+
This module provides centralized orchestration for all dependency installation
|
|
4
|
+
across different dependency types, with centralized user consent management
|
|
5
|
+
and delegation to specific installers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import datetime
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
15
|
+
|
|
16
|
+
from hatch_validator.package.package_service import PackageService
|
|
17
|
+
from hatch_validator.registry.registry_service import RegistryService
|
|
18
|
+
from hatch_validator.utils.hatch_dependency_graph import HatchDependencyGraphBuilder
|
|
19
|
+
from hatch_validator.utils.version_utils import VersionConstraintValidator, VersionConstraintError
|
|
20
|
+
from hatch_validator.core.validation_context import ValidationContext
|
|
21
|
+
|
|
22
|
+
from hatch.package_loader import HatchPackageLoader
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Mandatory to insure the installers are registered in the singleton `installer_registry` correctly at import time
|
|
26
|
+
from hatch.installers.hatch_installer import HatchInstaller
|
|
27
|
+
from hatch.installers.python_installer import PythonInstaller
|
|
28
|
+
from hatch.installers.system_installer import SystemInstaller
|
|
29
|
+
from hatch.installers.docker_installer import DockerInstaller
|
|
30
|
+
|
|
31
|
+
from hatch.installers.registry import installer_registry
|
|
32
|
+
from hatch.installers.installer_base import InstallationError
|
|
33
|
+
from hatch.installers.installation_context import InstallationContext, InstallationStatus
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DependencyInstallationError(Exception):
|
|
37
|
+
"""Exception raised for dependency installation-related errors."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DependencyInstallerOrchestrator:
|
|
42
|
+
"""Orchestrates dependency installation across all supported dependency types.
|
|
43
|
+
|
|
44
|
+
This class coordinates the installation of dependencies by:
|
|
45
|
+
1. Resolving all dependencies for a given package using the validator
|
|
46
|
+
2. Aggregating installation plans across all dependency types
|
|
47
|
+
3. Managing centralized user consent
|
|
48
|
+
4. Delegating to appropriate installers via the registry
|
|
49
|
+
5. Handling installation order and error recovery
|
|
50
|
+
|
|
51
|
+
The orchestrator strictly uses PackageService for all metadata access to ensure
|
|
52
|
+
compatibility across different package schema versions.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self,
|
|
56
|
+
package_loader: HatchPackageLoader,
|
|
57
|
+
registry_service: RegistryService,
|
|
58
|
+
registry_data: Dict[str, Any]):
|
|
59
|
+
"""Initialize the dependency installation orchestrator.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
package_loader (HatchPackageLoader): Package loader for file operations.
|
|
63
|
+
registry_service (RegistryService): Service for registry operations.
|
|
64
|
+
registry_data (Dict[str, Any]): Registry data for dependency resolution.
|
|
65
|
+
"""
|
|
66
|
+
self.logger = logging.getLogger("hatch.dependency_orchestrator")
|
|
67
|
+
self.logger.setLevel(logging.INFO)
|
|
68
|
+
self.package_loader = package_loader
|
|
69
|
+
self.registry_service = registry_service
|
|
70
|
+
self.registry_data = registry_data
|
|
71
|
+
|
|
72
|
+
# Python executable configuration for context
|
|
73
|
+
self._python_env_vars = Optional[Dict[str, str]] # Environment variables for Python execution
|
|
74
|
+
|
|
75
|
+
# These will be set during package resolution
|
|
76
|
+
self.package_service: Optional[PackageService] = None
|
|
77
|
+
self.dependency_graph_builder: Optional[HatchDependencyGraphBuilder] = None
|
|
78
|
+
self._resolved_package_path: Optional[Path] = None
|
|
79
|
+
self._resolved_package_type: Optional[str] = None
|
|
80
|
+
self._resolved_package_location: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
def set_python_env_vars(self, python_env_vars: Dict[str, str]) -> None:
|
|
83
|
+
"""Set the environment variables for the Python executable.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
python_env_vars (Dict[str, str]): Environment variables to set for Python execution.
|
|
87
|
+
"""
|
|
88
|
+
self._python_env_vars = python_env_vars
|
|
89
|
+
|
|
90
|
+
def get_python_env_vars(self) -> Optional[Dict[str, str]]:
|
|
91
|
+
"""Get the configured environment variables for the Python executable.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dict[str, str]: Environment variables for Python execution, None if not configured.
|
|
95
|
+
"""
|
|
96
|
+
return self._python_env_vars
|
|
97
|
+
|
|
98
|
+
def install_single_dep(self, dep: Dict[str, Any], context: InstallationContext) -> Dict[str, Any]:
|
|
99
|
+
"""Install a single dependency into the specified environment context.
|
|
100
|
+
|
|
101
|
+
This method installs a single dependency using the appropriate installer from the registry.
|
|
102
|
+
It extracts the core installation logic from _execute_install_plan for reuse in other contexts.
|
|
103
|
+
This method operates with auto_approve=True and does not require user consent.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
dep (Dict[str, Any]): Dependency dictionary following the schema for the dependency type.
|
|
107
|
+
For Python dependencies, should include: name, version_constraint, package_manager.
|
|
108
|
+
Example: {"name": "numpy", "version_constraint": "*", "package_manager": "pip", "type": "python"}
|
|
109
|
+
context (InstallationContext): Installation context with environment path and configuration.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dict[str, Any]: Installed package information containing:
|
|
113
|
+
- name: Package name
|
|
114
|
+
- version: Installed version
|
|
115
|
+
- type: Dependency type
|
|
116
|
+
- source: Package source URI
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
DependencyInstallationError: If installation fails or dependency type is not supported.
|
|
120
|
+
"""
|
|
121
|
+
# Ensure dependency has type information
|
|
122
|
+
dep_type = dep.get("type")
|
|
123
|
+
if not dep_type:
|
|
124
|
+
raise DependencyInstallationError(f"Dependency missing 'type' field: {dep}")
|
|
125
|
+
|
|
126
|
+
# Check if installer is registered for this dependency type
|
|
127
|
+
if not installer_registry.is_registered(dep_type):
|
|
128
|
+
raise DependencyInstallationError(f"No installer registered for dependency type: {dep_type}")
|
|
129
|
+
|
|
130
|
+
installer = installer_registry.get_installer(dep_type)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
self.logger.info(f"Installing {dep_type} dependency: {dep['name']}")
|
|
134
|
+
self.logger.debug(f"Dependency details: {dep}")
|
|
135
|
+
result = installer.install(dep, context)
|
|
136
|
+
if result.status == InstallationStatus.COMPLETED:
|
|
137
|
+
installed_package = {
|
|
138
|
+
"name": dep["name"],
|
|
139
|
+
"version": dep.get("resolved_version", dep.get("version")),
|
|
140
|
+
"type": dep_type,
|
|
141
|
+
"source": dep.get("uri", "unknown")
|
|
142
|
+
}
|
|
143
|
+
self.logger.info(f"Successfully installed {dep_type} dependency: {dep['name']}")
|
|
144
|
+
return installed_package
|
|
145
|
+
else:
|
|
146
|
+
raise DependencyInstallationError(f"Failed to install {dep['name']}: {result.error_message}")
|
|
147
|
+
|
|
148
|
+
except InstallationError as e:
|
|
149
|
+
self.logger.error(f"Installation error for {dep_type} dependency {dep['name']}: {e.error_code}\n{e.message}")
|
|
150
|
+
raise DependencyInstallationError(f"Installation error for {dep['name']}: {e}") from e
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
self.logger.error(f"Error installing {dep_type} dependency {dep['name']}: {e}")
|
|
154
|
+
raise DependencyInstallationError(f"Error installing {dep['name']}: {e}") from e
|
|
155
|
+
|
|
156
|
+
def install_dependencies(self,
|
|
157
|
+
package_path_or_name: str,
|
|
158
|
+
env_path: Path,
|
|
159
|
+
env_name: str,
|
|
160
|
+
existing_packages: Dict[str, str],
|
|
161
|
+
version_constraint: Optional[str] = None,
|
|
162
|
+
force_download: bool = False,
|
|
163
|
+
auto_approve: bool = False) -> Tuple[bool, List[Dict[str, Any]]]:
|
|
164
|
+
"""Install all dependencies for a package with centralized consent management.
|
|
165
|
+
|
|
166
|
+
This method orchestrates the complete dependency installation process by
|
|
167
|
+
leveraging existing validator components and the installer registry. It handles
|
|
168
|
+
all dependency types (hatch, python, system, docker) and provides centralized
|
|
169
|
+
user consent management.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
package_path_or_name (str): Path to local package or name of remote package.
|
|
173
|
+
env_path (Path): Path to the environment directory.
|
|
174
|
+
env_name (str): Name of the environment.
|
|
175
|
+
existing_packages (Dict[str, str]): Currently installed packages {name: version}.
|
|
176
|
+
version_constraint (str, optional): Version constraint for remote packages. Defaults to None.
|
|
177
|
+
force_download (bool, optional): Force download even if package is cached. Defaults to False.
|
|
178
|
+
auto_approve (bool, optional): Skip user consent prompt for automation. Defaults to False.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Tuple[bool, List[Dict[str, Any]]]: Success status and list of installed packages.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
DependencyInstallationError: If installation fails at any stage.
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
# Step 1: Resolve package and load metadata using PackageService
|
|
188
|
+
self._resolve_and_load_package(package_path_or_name, version_constraint, force_download)
|
|
189
|
+
|
|
190
|
+
# Step 2: Get all dependencies organized by type
|
|
191
|
+
dependencies_by_type = self._get_all_dependencies()
|
|
192
|
+
|
|
193
|
+
# Step 3: Filter for missing dependencies by type and track satisfied ones
|
|
194
|
+
missing_dependencies_by_type, satisfied_dependencies_by_type = self._filter_missing_dependencies_by_type(dependencies_by_type, existing_packages)
|
|
195
|
+
|
|
196
|
+
# Step 4: Aggregate installation plan
|
|
197
|
+
install_plan = self._aggregate_install_plan(missing_dependencies_by_type, satisfied_dependencies_by_type)
|
|
198
|
+
|
|
199
|
+
# Step 5: Print installation summary for user review
|
|
200
|
+
self._print_installation_summary(install_plan)
|
|
201
|
+
|
|
202
|
+
# Step 6: Request user consent
|
|
203
|
+
if not auto_approve:
|
|
204
|
+
if not self._request_user_consent(install_plan):
|
|
205
|
+
self.logger.info("Installation cancelled by user")
|
|
206
|
+
return False, []
|
|
207
|
+
else:
|
|
208
|
+
self.logger.warning("Auto-approval enabled, proceeding with installation without user consent")
|
|
209
|
+
|
|
210
|
+
# Step 7: Execute installation plan using installer registry
|
|
211
|
+
installed_packages = self._execute_install_plan(install_plan, env_path, env_name)
|
|
212
|
+
|
|
213
|
+
return True, installed_packages
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
self.logger.error(f"Dependency installation failed: {e}")
|
|
217
|
+
raise DependencyInstallationError(f"Installation failed: {e}") from e
|
|
218
|
+
|
|
219
|
+
def _resolve_and_load_package(self,
|
|
220
|
+
package_path_or_name: str,
|
|
221
|
+
version_constraint: Optional[str] = None,
|
|
222
|
+
force_download: bool = False) -> None:
|
|
223
|
+
"""Resolve package information and load metadata using PackageService.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
package_path_or_name (str): Path to local package or name of remote package.
|
|
227
|
+
version_constraint (str, optional): Version constraint for remote packages.
|
|
228
|
+
force_download (bool, optional): Force download even if package is cached.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
DependencyInstallationError: If package cannot be resolved or loaded.
|
|
232
|
+
"""
|
|
233
|
+
path = Path(package_path_or_name)
|
|
234
|
+
|
|
235
|
+
if path.exists() and path.is_dir():
|
|
236
|
+
# Local package
|
|
237
|
+
metadata_path = path / "hatch_metadata.json"
|
|
238
|
+
if not metadata_path.exists():
|
|
239
|
+
raise DependencyInstallationError(f"Local package missing hatch_metadata.json: {path}")
|
|
240
|
+
|
|
241
|
+
with open(metadata_path, 'r') as f:
|
|
242
|
+
metadata = json.load(f)
|
|
243
|
+
|
|
244
|
+
self._resolved_package_path = path
|
|
245
|
+
self._resolved_package_type = "local"
|
|
246
|
+
self._resolved_package_location = str(path.resolve())
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
# Remote package
|
|
250
|
+
if not self.registry_service.package_exists(package_path_or_name):
|
|
251
|
+
raise DependencyInstallationError(f"Package {package_path_or_name} does not exist in registry")
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
compatible_version = self.registry_service.find_compatible_version(
|
|
255
|
+
package_path_or_name, version_constraint)
|
|
256
|
+
except VersionConstraintError as e:
|
|
257
|
+
raise DependencyInstallationError(f"Version constraint error: {e}") from e
|
|
258
|
+
|
|
259
|
+
location = self.registry_service.get_package_uri(package_path_or_name, compatible_version)
|
|
260
|
+
downloaded_path = self.package_loader.download_package(
|
|
261
|
+
location, package_path_or_name, compatible_version, force_download=force_download)
|
|
262
|
+
|
|
263
|
+
metadata_path = downloaded_path / "hatch_metadata.json"
|
|
264
|
+
with open(metadata_path, 'r') as f:
|
|
265
|
+
metadata = json.load(f)
|
|
266
|
+
|
|
267
|
+
self._resolved_package_path = downloaded_path
|
|
268
|
+
self._resolved_package_type = "remote"
|
|
269
|
+
self._resolved_package_location = location
|
|
270
|
+
|
|
271
|
+
# Load metadata using PackageService for schema-aware access
|
|
272
|
+
self.package_service = PackageService(metadata)
|
|
273
|
+
if not self.package_service.is_loaded():
|
|
274
|
+
raise DependencyInstallationError("Failed to load package metadata")
|
|
275
|
+
|
|
276
|
+
def _get_install_ready_hatch_dependencies(self) -> List[Dict[str, Any]]:
|
|
277
|
+
"""Get install-ready Hatch dependencies using validator components.
|
|
278
|
+
|
|
279
|
+
This method only processes Hatch package dependencies, not python, system, or docker.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
List[Dict[str, Any]]: List of install-ready Hatch dependencies.
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
DependencyInstallationError: If dependency resolution fails.
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
# Use validator components for Hatch dependency resolution
|
|
289
|
+
self.dependency_graph_builder = HatchDependencyGraphBuilder(
|
|
290
|
+
self.package_service, self.registry_service)
|
|
291
|
+
|
|
292
|
+
context = ValidationContext(
|
|
293
|
+
package_dir=self._resolved_package_path,
|
|
294
|
+
registry_data=self.registry_data,
|
|
295
|
+
allow_local_dependencies=True
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# This only returns Hatch dependencies in install order
|
|
299
|
+
hatch_dependencies = self.dependency_graph_builder.get_install_ready_dependencies(context)
|
|
300
|
+
return hatch_dependencies
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
raise DependencyInstallationError(f"Error building Hatch dependency graph: {e}") from e
|
|
304
|
+
|
|
305
|
+
def _get_all_dependencies(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
306
|
+
"""Get all dependencies from package metadata organized by type.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Dict[str, List[Dict[str, Any]]]: Dependencies organized by type (hatch, python, system, docker).
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
DependencyInstallationError: If dependency extraction fails.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
# Get all dependencies using PackageService
|
|
316
|
+
all_deps = self.package_service.get_dependencies()
|
|
317
|
+
|
|
318
|
+
dependencies_by_type = {
|
|
319
|
+
"system": [],
|
|
320
|
+
"python": [],
|
|
321
|
+
"hatch": [],
|
|
322
|
+
"docker": []
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Get Hatch dependencies using validator (properly ordered)
|
|
326
|
+
dependencies_by_type["hatch"] = self._get_install_ready_hatch_dependencies()
|
|
327
|
+
# Adding the type information to each Hatch dependency
|
|
328
|
+
for dep in dependencies_by_type["hatch"]:
|
|
329
|
+
dep["type"] = "hatch"
|
|
330
|
+
|
|
331
|
+
# Get other dependency types directly from PackageService
|
|
332
|
+
for dep_type in ["python", "system", "docker"]:
|
|
333
|
+
raw_deps = all_deps.get(dep_type, [])
|
|
334
|
+
for dep in raw_deps:
|
|
335
|
+
|
|
336
|
+
# Add type information and ensure required fields
|
|
337
|
+
dep_with_type = dep.copy()
|
|
338
|
+
dep_with_type["type"] = dep_type
|
|
339
|
+
if not installer_registry.can_install(dep_type, dep_with_type):
|
|
340
|
+
raise DependencyInstallationError(
|
|
341
|
+
f"No registered installer can handle dependency with type '{dep_type}': {dep_with_type}"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
dependencies_by_type[dep_type].append(dep_with_type)
|
|
345
|
+
|
|
346
|
+
return dependencies_by_type
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
raise DependencyInstallationError(f"Error extracting dependencies: {e}") from e
|
|
350
|
+
|
|
351
|
+
def _filter_missing_dependencies_by_type(self,
|
|
352
|
+
dependencies_by_type: Dict[str, List[Dict[str, Any]]],
|
|
353
|
+
existing_packages: Dict[str, str]) -> Tuple[Dict[str, List[Dict[str, Any]]], Dict[str, List[Dict[str, Any]]]]:
|
|
354
|
+
"""Filter dependencies by type to find those not already installed and track satisfied ones.
|
|
355
|
+
|
|
356
|
+
For non-Hatch dependencies, we always include them in missing list as the third-party
|
|
357
|
+
package manager will handle version checking and installation.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
dependencies_by_type (Dict[str, List[Dict[str, Any]]]): All dependencies organized by type.
|
|
361
|
+
existing_packages (Dict[str, str]): Currently installed packages {name: version}.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Tuple[Dict[str, List[Dict[str, Any]]], Dict[str, List[Dict[str, Any]]]]:
|
|
365
|
+
(missing_dependencies_by_type, satisfied_dependencies_by_type)
|
|
366
|
+
"""
|
|
367
|
+
missing_deps_by_type = {}
|
|
368
|
+
satisfied_deps_by_type = {}
|
|
369
|
+
|
|
370
|
+
for dep_type, dependencies in dependencies_by_type.items():
|
|
371
|
+
missing_deps = []
|
|
372
|
+
satisfied_deps = []
|
|
373
|
+
|
|
374
|
+
for dep in dependencies:
|
|
375
|
+
dep_name = dep.get("name")
|
|
376
|
+
|
|
377
|
+
# For non-Hatch dependencies, always consider them as needing installation
|
|
378
|
+
# as the third-party package manager will handle version compatibility
|
|
379
|
+
if dep_type != "hatch":
|
|
380
|
+
missing_deps.append(dep)
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# Hatch dependency processing
|
|
384
|
+
if dep_name not in existing_packages:
|
|
385
|
+
missing_deps.append(dep)
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
# Check version constraints for Hatch dependencies
|
|
389
|
+
constraint = dep.get("version_constraint")
|
|
390
|
+
installed_version = existing_packages[dep_name]
|
|
391
|
+
|
|
392
|
+
if constraint:
|
|
393
|
+
is_compatible, compatibility_msg = VersionConstraintValidator.is_version_compatible(
|
|
394
|
+
installed_version, constraint)
|
|
395
|
+
if not is_compatible:
|
|
396
|
+
missing_deps.append(dep)
|
|
397
|
+
else:
|
|
398
|
+
# Add satisfied dependency with installation info
|
|
399
|
+
satisfied_dep = dep.copy()
|
|
400
|
+
satisfied_dep["installed_version"] = installed_version
|
|
401
|
+
satisfied_dep["compatibility_status"] = compatibility_msg
|
|
402
|
+
satisfied_deps.append(satisfied_dep)
|
|
403
|
+
else:
|
|
404
|
+
# No constraint specified, any installed version satisfies
|
|
405
|
+
satisfied_dep = dep.copy()
|
|
406
|
+
satisfied_dep["installed_version"] = installed_version
|
|
407
|
+
satisfied_dep["compatibility_status"] = "No version constraint specified"
|
|
408
|
+
satisfied_deps.append(satisfied_dep)
|
|
409
|
+
|
|
410
|
+
missing_deps_by_type[dep_type] = missing_deps
|
|
411
|
+
satisfied_deps_by_type[dep_type] = satisfied_deps
|
|
412
|
+
|
|
413
|
+
return missing_deps_by_type, satisfied_deps_by_type
|
|
414
|
+
|
|
415
|
+
def _aggregate_install_plan(self,
|
|
416
|
+
missing_dependencies_by_type: Dict[str, List[Dict[str, Any]]],
|
|
417
|
+
satisfied_dependencies_by_type: Dict[str, List[Dict[str, Any]]]) -> Dict[str, Any]:
|
|
418
|
+
"""Aggregate installation plan across all dependency types.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
missing_dependencies_by_type (Dict[str, List[Dict[str, Any]]]): Missing dependencies by type.
|
|
422
|
+
satisfied_dependencies_by_type (Dict[str, List[Dict[str, Any]]]): Already satisfied dependencies by type.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Dict[str, Any]: Complete installation plan with dependencies grouped by type.
|
|
426
|
+
"""
|
|
427
|
+
# Use PackageService for all metadata access
|
|
428
|
+
plan = {
|
|
429
|
+
"main_package": {
|
|
430
|
+
"name": self.package_service.get_field("name"),
|
|
431
|
+
"version": self.package_service.get_field("version"),
|
|
432
|
+
"type": self._resolved_package_type,
|
|
433
|
+
"location": self._resolved_package_location
|
|
434
|
+
},
|
|
435
|
+
"dependencies_to_install": missing_dependencies_by_type,
|
|
436
|
+
"dependencies_satisfied": satisfied_dependencies_by_type,
|
|
437
|
+
"total_to_install": 1 + sum(len(deps) for deps in missing_dependencies_by_type.values()),
|
|
438
|
+
"total_satisfied": sum(len(deps) for deps in satisfied_dependencies_by_type.values())
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return plan
|
|
442
|
+
|
|
443
|
+
def _print_installation_summary(self, install_plan: Dict[str, Any]) -> None:
|
|
444
|
+
"""Print a summary of the installation plan for user review.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
install_plan (Dict[str, Any]): Complete installation plan.
|
|
448
|
+
"""
|
|
449
|
+
print("\n" + "="*60)
|
|
450
|
+
print("DEPENDENCY INSTALLATION PLAN")
|
|
451
|
+
print("="*60)
|
|
452
|
+
|
|
453
|
+
main_pkg = install_plan['main_package']
|
|
454
|
+
print(f"Main Package: {main_pkg['name']} v{main_pkg['version']}")
|
|
455
|
+
print(f"Package Type: {main_pkg['type']}")
|
|
456
|
+
|
|
457
|
+
# Show satisfied dependencies first
|
|
458
|
+
total_satisfied = install_plan.get("total_satisfied", 0)
|
|
459
|
+
if total_satisfied > 0:
|
|
460
|
+
print(f"\nDependencies already satisfied: {total_satisfied}")
|
|
461
|
+
|
|
462
|
+
for dep_type, deps in install_plan.get("dependencies_satisfied", {}).items():
|
|
463
|
+
if deps:
|
|
464
|
+
print(f"\n{dep_type.title()} Dependencies (Satisfied):")
|
|
465
|
+
for dep in deps:
|
|
466
|
+
installed_version = dep.get("installed_version", "unknown")
|
|
467
|
+
constraint = dep.get("version_constraint", "any")
|
|
468
|
+
compatibility = dep.get("compatibility_status", "")
|
|
469
|
+
print(f" ✓ {dep['name']} {constraint} (installed: {installed_version})")
|
|
470
|
+
if compatibility and compatibility != "No version constraint specified":
|
|
471
|
+
print(f" {compatibility}")
|
|
472
|
+
|
|
473
|
+
# Show dependencies to install
|
|
474
|
+
total_to_install = sum(len(deps) for deps in install_plan.get("dependencies_to_install", {}).values())
|
|
475
|
+
if total_to_install > 0:
|
|
476
|
+
print(f"\nDependencies to install: {total_to_install}")
|
|
477
|
+
|
|
478
|
+
for dep_type, deps in install_plan.get("dependencies_to_install", {}).items():
|
|
479
|
+
if deps:
|
|
480
|
+
print(f"\n{dep_type.title()} Dependencies (To Install):")
|
|
481
|
+
for dep in deps:
|
|
482
|
+
constraint = dep.get("version_constraint", "any")
|
|
483
|
+
print(f" → {dep['name']} {constraint}")
|
|
484
|
+
else:
|
|
485
|
+
print("\nNo additional dependencies to install.")
|
|
486
|
+
|
|
487
|
+
print(f"\nTotal packages to install: {install_plan.get('total_to_install', 1)}")
|
|
488
|
+
if total_satisfied > 0:
|
|
489
|
+
print(f"Total dependencies already satisfied: {total_satisfied}")
|
|
490
|
+
print("="*60)
|
|
491
|
+
|
|
492
|
+
def _request_user_consent(self, install_plan: Dict[str, Any]) -> bool:
|
|
493
|
+
"""Request user consent for the installation plan with non-TTY support.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
install_plan (Dict[str, Any]): Complete installation plan.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
bool: True if user approves, False otherwise.
|
|
500
|
+
"""
|
|
501
|
+
# Check for non-interactive mode indicators
|
|
502
|
+
if (not sys.stdin.isatty() or
|
|
503
|
+
os.getenv('HATCH_AUTO_APPROVE', '').lower() in ('1', 'true', 'yes')):
|
|
504
|
+
|
|
505
|
+
self.logger.info("Auto-approving installation (non-interactive mode)")
|
|
506
|
+
return True
|
|
507
|
+
|
|
508
|
+
# Interactive mode - request user input
|
|
509
|
+
try:
|
|
510
|
+
while True:
|
|
511
|
+
response = input("\nProceed with installation? [y/N]: ").strip().lower()
|
|
512
|
+
if response in ['y', 'yes']:
|
|
513
|
+
return True
|
|
514
|
+
elif response in ['n', 'no', '']:
|
|
515
|
+
return False
|
|
516
|
+
else:
|
|
517
|
+
print("Please enter 'y' for yes or 'n' for no.")
|
|
518
|
+
except (EOFError, KeyboardInterrupt):
|
|
519
|
+
self.logger.info("Installation cancelled by user")
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
def _execute_install_plan(self,
|
|
523
|
+
install_plan: Dict[str, Any],
|
|
524
|
+
env_path: Path,
|
|
525
|
+
env_name: str) -> List[Dict[str, Any]]:
|
|
526
|
+
"""Execute the installation plan using the installer registry.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
install_plan (Dict[str, Any]): Installation plan to execute.
|
|
530
|
+
env_path (Path): Environment path for installation.
|
|
531
|
+
env_name (str): Environment name.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
List[Dict[str, Any]]: List of successfully installed packages.
|
|
535
|
+
|
|
536
|
+
Raises:
|
|
537
|
+
DependencyInstallationError: If installation fails.
|
|
538
|
+
"""
|
|
539
|
+
installed_packages = []
|
|
540
|
+
|
|
541
|
+
# Create comprehensive installation context
|
|
542
|
+
context = InstallationContext(
|
|
543
|
+
environment_path=env_path,
|
|
544
|
+
environment_name=env_name,
|
|
545
|
+
temp_dir=env_path / ".tmp",
|
|
546
|
+
cache_dir=self.package_loader.cache_dir if hasattr(self.package_loader, 'cache_dir') else None,
|
|
547
|
+
parallel_enabled=False, # Future enhancement
|
|
548
|
+
force_reinstall=False, # Future enhancement
|
|
549
|
+
simulation_mode=False, # Future enhancement
|
|
550
|
+
extra_config={
|
|
551
|
+
"package_loader": self.package_loader,
|
|
552
|
+
"registry_service": self.registry_service,
|
|
553
|
+
"registry_data": self.registry_data,
|
|
554
|
+
"main_package_path": self._resolved_package_path,
|
|
555
|
+
"main_package_type": self._resolved_package_type
|
|
556
|
+
}
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Configure Python environment variables if available
|
|
560
|
+
if self._python_env_vars:
|
|
561
|
+
context.set_config("python_env_vars", self._python_env_vars)
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
# Install dependencies by type using appropriate installers
|
|
565
|
+
for dep_type, dependencies in install_plan["dependencies_to_install"].items():
|
|
566
|
+
if not dependencies:
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
if not installer_registry.is_registered(dep_type):
|
|
570
|
+
self.logger.warning(f"No installer registered for dependency type: {dep_type}")
|
|
571
|
+
continue
|
|
572
|
+
|
|
573
|
+
installer = installer_registry.get_installer(dep_type)
|
|
574
|
+
|
|
575
|
+
for dep in dependencies:
|
|
576
|
+
# Use the extracted install_single_dep method
|
|
577
|
+
installed_package = self.install_single_dep(dep, context)
|
|
578
|
+
installed_packages.append(installed_package)
|
|
579
|
+
|
|
580
|
+
# Install main package last
|
|
581
|
+
main_pkg_info = self._install_main_package(context)
|
|
582
|
+
installed_packages.append(main_pkg_info)
|
|
583
|
+
|
|
584
|
+
return installed_packages
|
|
585
|
+
|
|
586
|
+
except Exception as e:
|
|
587
|
+
self.logger.error(f"Installation execution failed: {e}")
|
|
588
|
+
raise DependencyInstallationError(f"Installation execution failed: {e}") from e
|
|
589
|
+
|
|
590
|
+
def _install_main_package(self, context: InstallationContext) -> Dict[str, Any]:
|
|
591
|
+
"""Install the main package using package_loader directly.
|
|
592
|
+
|
|
593
|
+
The main package installation bypasses the installer registry and uses
|
|
594
|
+
the package_loader directly since it's not a dependency but the primary package.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
context (InstallationContext): Installation context.
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Dict[str, Any]: Installed package information.
|
|
601
|
+
|
|
602
|
+
Raises:
|
|
603
|
+
DependencyInstallationError: If main package installation fails.
|
|
604
|
+
"""
|
|
605
|
+
try:
|
|
606
|
+
# Get package information using PackageService
|
|
607
|
+
package_name = self.package_service.get_field("name")
|
|
608
|
+
package_version = self.package_service.get_field("version")
|
|
609
|
+
|
|
610
|
+
# Install using package_loader directly
|
|
611
|
+
if self._resolved_package_type == "local":
|
|
612
|
+
# For local packages, install from resolved path
|
|
613
|
+
installed_path = self.package_loader.install_local_package(
|
|
614
|
+
source_path=self._resolved_package_path,
|
|
615
|
+
target_dir=context.environment_path,
|
|
616
|
+
package_name=package_name
|
|
617
|
+
)
|
|
618
|
+
else:
|
|
619
|
+
# For remote packages, install from downloaded path
|
|
620
|
+
installed_path = self.package_loader.install_local_package(
|
|
621
|
+
source_path=self._resolved_package_path, # Downloaded path
|
|
622
|
+
target_dir=context.environment_path,
|
|
623
|
+
package_name=package_name
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
self.logger.info(f"Successfully installed main package {package_name} to {installed_path}")
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
"name": package_name,
|
|
630
|
+
"version": package_version,
|
|
631
|
+
"type": "hatch",
|
|
632
|
+
"source": self._resolved_package_location
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
except Exception as e:
|
|
636
|
+
raise DependencyInstallationError(f"Failed to install main package: {e}") from e
|