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,198 @@
|
|
|
1
|
+
"""Installer for Hatch package dependencies.
|
|
2
|
+
|
|
3
|
+
Implements installation logic for Hatch packages using the HatchPackageLoader and
|
|
4
|
+
integrates pre-install validation using HatchPackageValidator and PackageService.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Any, Optional, Callable, List
|
|
11
|
+
|
|
12
|
+
from hatch.installers.installer_base import DependencyInstaller, InstallationContext, InstallationResult, InstallationError
|
|
13
|
+
from hatch.installers.installation_context import InstallationStatus
|
|
14
|
+
from hatch.package_loader import HatchPackageLoader, PackageLoaderError
|
|
15
|
+
from hatch_validator.package_validator import HatchPackageValidator
|
|
16
|
+
|
|
17
|
+
class HatchInstaller(DependencyInstaller):
|
|
18
|
+
"""Installer for Hatch package dependencies.
|
|
19
|
+
|
|
20
|
+
Handles installation, validation, and uninstallation of Hatch packages using
|
|
21
|
+
the HatchPackageLoader and validator APIs.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, registry_data: Optional[Dict[str, Any]] = None):
|
|
25
|
+
"""Initialize the HatchInstaller.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
registry_data (Dict[str, Any], optional): Registry data for validation. Defaults to None.
|
|
29
|
+
"""
|
|
30
|
+
self.logger = logging.getLogger("hatch.installers.hatch_installer")
|
|
31
|
+
self.package_loader = HatchPackageLoader()
|
|
32
|
+
self.validator = HatchPackageValidator(registry_data=registry_data)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def installer_type(self) -> str:
|
|
36
|
+
"""Get the type identifier for this installer.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
str: Unique identifier for the installer type ("hatch").
|
|
40
|
+
"""
|
|
41
|
+
return "hatch"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def supported_schemes(self) -> List[str]:
|
|
45
|
+
"""Get the URI schemes this installer can handle.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List[str]: List of URI schemes (e.g., ["file", "http", "https"]).
|
|
49
|
+
"""
|
|
50
|
+
return ["file", "http", "https"]
|
|
51
|
+
|
|
52
|
+
def can_install(self, dependency: Dict[str, Any]) -> bool:
|
|
53
|
+
"""Check if this installer can handle the given dependency.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
dependency (Dict[str, Any]): Dependency object.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
bool: True if this installer can handle the dependency, False otherwise.
|
|
60
|
+
"""
|
|
61
|
+
return dependency.get("type") == self.installer_type
|
|
62
|
+
|
|
63
|
+
def validate_dependency(self, dependency: Dict[str, Any]) -> bool:
|
|
64
|
+
"""Validate that a dependency object has required fields and is a valid Hatch package.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
dependency (Dict[str, Any]): Dependency object to validate.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
bool: True if dependency is valid, False otherwise.
|
|
71
|
+
"""
|
|
72
|
+
required_fields = ["name", "version_constraint", "resolved_version", "uri"]
|
|
73
|
+
if not all(field in dependency for field in required_fields):
|
|
74
|
+
return False
|
|
75
|
+
# Optionally, perform further validation using the validator if a path is provided
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def install(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
79
|
+
progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
|
|
80
|
+
"""Install a Hatch package dependency.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
dependency (Dict[str, Any]): Dependency object containing name, version, uri, etc.
|
|
84
|
+
context (InstallationContext): Installation context with environment info.
|
|
85
|
+
progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
InstallationResult: Result of the installation operation.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
InstallationError: If installation fails for any reason.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
self.logger.debug(f"Installing Hatch dependency: {dependency}")
|
|
95
|
+
if not self.validate_dependency(dependency):
|
|
96
|
+
self.logger.error(f"Invalid dependency format: {dependency}")
|
|
97
|
+
raise InstallationError("Invalid dependency object",
|
|
98
|
+
dependency_name=dependency.get("name"),
|
|
99
|
+
error_code="INVALID_HATCH_DEPENDENCY_FORMAT",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
name = dependency["name"]
|
|
103
|
+
version = dependency["resolved_version"]
|
|
104
|
+
uri = dependency["uri"]
|
|
105
|
+
target_dir = Path(context.environment_path)
|
|
106
|
+
try:
|
|
107
|
+
if progress_callback:
|
|
108
|
+
progress_callback("install", 0.0, f"Installing {name}-{version} from {uri}")
|
|
109
|
+
# Download/install the package
|
|
110
|
+
if uri and uri.startswith("file://"):
|
|
111
|
+
pkg_path = Path(uri[7:])
|
|
112
|
+
result_path = self.package_loader.install_local_package(pkg_path, target_dir, name)
|
|
113
|
+
elif uri:
|
|
114
|
+
result_path = self.package_loader.install_remote_package(uri, name, version, target_dir)
|
|
115
|
+
else:
|
|
116
|
+
raise InstallationError(f"No URI provided for dependency {name}", dependency_name=name)
|
|
117
|
+
|
|
118
|
+
if progress_callback:
|
|
119
|
+
progress_callback("install", 1.0, f"Installed {name} to {result_path}")
|
|
120
|
+
|
|
121
|
+
return InstallationResult(
|
|
122
|
+
dependency_name=name,
|
|
123
|
+
status=InstallationStatus.COMPLETED,
|
|
124
|
+
installed_path=result_path,
|
|
125
|
+
installed_version=version,
|
|
126
|
+
error_message=None,
|
|
127
|
+
artifacts=result_path,
|
|
128
|
+
metadata={"name": name, "version": version}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
except (PackageLoaderError, Exception) as e:
|
|
132
|
+
self.logger.error(f"Failed to install {name}: {e}")
|
|
133
|
+
raise InstallationError(f"Failed to install {name}: {e}", dependency_name=name, cause=e)
|
|
134
|
+
|
|
135
|
+
def uninstall(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
136
|
+
progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
|
|
137
|
+
"""Uninstall a Hatch package dependency.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
dependency (Dict[str, Any]): Dependency object to uninstall.
|
|
141
|
+
context (InstallationContext): Installation context with environment info.
|
|
142
|
+
progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
InstallationResult: Result of the uninstall operation.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
InstallationError: If uninstall fails for any reason.
|
|
149
|
+
"""
|
|
150
|
+
if not self.validate_dependency(dependency):
|
|
151
|
+
raise InstallationError("Invalid dependency object",
|
|
152
|
+
dependency_name=dependency.get("name"),
|
|
153
|
+
error_code="INVALID_HATCH_DEPENDENCY_FORMAT",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
name = dependency["name"]
|
|
157
|
+
target_dir = Path(context.environment_path) / name
|
|
158
|
+
try:
|
|
159
|
+
if target_dir.exists():
|
|
160
|
+
shutil.rmtree(target_dir)
|
|
161
|
+
if progress_callback:
|
|
162
|
+
progress_callback("uninstall", 1.0, f"Uninstalled {name}")
|
|
163
|
+
return InstallationResult(
|
|
164
|
+
dependency_name=name,
|
|
165
|
+
status=InstallationStatus.COMPLETED,
|
|
166
|
+
installed_path=target_dir,
|
|
167
|
+
installed_version=dependency.get("resolved_version"),
|
|
168
|
+
error_message=None,
|
|
169
|
+
artifacts=None,
|
|
170
|
+
metadata={"name": name}
|
|
171
|
+
)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
self.logger.error(f"Failed to uninstall {name}: {e}")
|
|
174
|
+
raise InstallationError(f"Failed to uninstall {name}: {e}", dependency_name=name, cause=e)
|
|
175
|
+
|
|
176
|
+
def cleanup_failed_installation(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
177
|
+
artifacts: Optional[List[Path]] = None) -> None:
|
|
178
|
+
"""Clean up artifacts from a failed installation.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
dependency (Dict[str, Any]): Dependency that failed to install.
|
|
182
|
+
context (InstallationContext): Installation context.
|
|
183
|
+
artifacts (List[Path], optional): List of files/directories to clean up.
|
|
184
|
+
"""
|
|
185
|
+
if artifacts:
|
|
186
|
+
for artifact in artifacts:
|
|
187
|
+
try:
|
|
188
|
+
if artifact.exists():
|
|
189
|
+
if artifact.is_file():
|
|
190
|
+
artifact.unlink()
|
|
191
|
+
elif artifact.is_dir():
|
|
192
|
+
shutil.rmtree(artifact)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
# Register this installer with the global registry
|
|
197
|
+
from .registry import installer_registry
|
|
198
|
+
installer_registry.register_installer("hatch", HatchInstaller)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defines context, status, and result data structures for dependency installation.
|
|
3
|
+
|
|
4
|
+
This module provides the InstallationContext dataclass for encapsulating
|
|
5
|
+
environment and configuration information required during dependency installation,
|
|
6
|
+
as well as InstallationStatus and InstallationResult for representing the
|
|
7
|
+
outcome and details of installation operations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Any, Optional, List
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class InstallationContext:
|
|
17
|
+
"""Context information for dependency installation.
|
|
18
|
+
|
|
19
|
+
This class encapsulates all the environment and configuration information
|
|
20
|
+
needed for installing dependencies, making the installer interface cleaner
|
|
21
|
+
and more extensible.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
environment_path: Path
|
|
25
|
+
"""Path to the target environment where dependencies will be installed."""
|
|
26
|
+
|
|
27
|
+
environment_name: str
|
|
28
|
+
"""Name of the target environment."""
|
|
29
|
+
|
|
30
|
+
temp_dir: Optional[Path] = None
|
|
31
|
+
"""Temporary directory for download/build operations."""
|
|
32
|
+
|
|
33
|
+
cache_dir: Optional[Path] = None
|
|
34
|
+
"""Cache directory for reusable artifacts."""
|
|
35
|
+
|
|
36
|
+
parallel_enabled: bool = True
|
|
37
|
+
"""Whether parallel installation is enabled."""
|
|
38
|
+
|
|
39
|
+
force_reinstall: bool = False
|
|
40
|
+
"""Whether to force reinstallation of existing packages."""
|
|
41
|
+
|
|
42
|
+
simulation_mode: bool = False
|
|
43
|
+
"""Whether to run in simulation mode (no actual installation)."""
|
|
44
|
+
|
|
45
|
+
extra_config: Optional[Dict[str, Any]] = None
|
|
46
|
+
"""Additional installer-specific configuration."""
|
|
47
|
+
|
|
48
|
+
def get_config(self, key: str, default: Any = None) -> Any:
|
|
49
|
+
"""Get a configuration value from extra_config.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
key (str): Configuration key to retrieve.
|
|
53
|
+
default (Any, optional): Default value if key not found.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Any: Configuration value or default.
|
|
57
|
+
"""
|
|
58
|
+
if self.extra_config is None:
|
|
59
|
+
return default
|
|
60
|
+
return self.extra_config.get(key, default)
|
|
61
|
+
|
|
62
|
+
def set_config(self, key: str, value: Any) -> None:
|
|
63
|
+
"""Set a configuration value in extra_config.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
key (str): Configuration key to set.
|
|
67
|
+
value (Any): Value to set for the key.
|
|
68
|
+
"""
|
|
69
|
+
if self.extra_config is None:
|
|
70
|
+
self.extra_config = {}
|
|
71
|
+
self.extra_config[key] = value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class InstallationStatus(Enum):
|
|
75
|
+
"""Status of an installation operation."""
|
|
76
|
+
PENDING = "pending"
|
|
77
|
+
IN_PROGRESS = "in_progress"
|
|
78
|
+
COMPLETED = "completed"
|
|
79
|
+
FAILED = "failed"
|
|
80
|
+
ROLLED_BACK = "rolled_back"
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class InstallationResult:
|
|
84
|
+
"""Result of an installation operation.
|
|
85
|
+
|
|
86
|
+
Provides detailed information about the installation outcome,
|
|
87
|
+
including status, paths, and any error information.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
dependency_name: str
|
|
91
|
+
"""Name of the dependency that was installed."""
|
|
92
|
+
|
|
93
|
+
status: InstallationStatus
|
|
94
|
+
"""Final status of the installation."""
|
|
95
|
+
|
|
96
|
+
installed_path: Optional[Path] = None
|
|
97
|
+
"""Path where the dependency was installed."""
|
|
98
|
+
|
|
99
|
+
installed_version: Optional[str] = None
|
|
100
|
+
"""Actual version that was installed."""
|
|
101
|
+
|
|
102
|
+
error_message: Optional[str] = None
|
|
103
|
+
"""Error message if installation failed."""
|
|
104
|
+
|
|
105
|
+
artifacts: Optional[List[Path]] = None
|
|
106
|
+
"""List of files/directories created during installation."""
|
|
107
|
+
|
|
108
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
109
|
+
"""Additional installer-specific metadata."""
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Abstract base class for dependency installers.
|
|
2
|
+
|
|
3
|
+
This module defines the core installer interface that all concrete installers
|
|
4
|
+
must implement, ensuring consistent behavior across different dependency types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional, Callable, List
|
|
10
|
+
|
|
11
|
+
from .installation_context import InstallationContext, InstallationResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InstallationError(Exception):
|
|
15
|
+
"""Exception raised for installation-related errors.
|
|
16
|
+
|
|
17
|
+
This exception provides structured error information that can be used
|
|
18
|
+
for error reporting and recovery strategies.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, dependency_name: Optional[str] = None,
|
|
22
|
+
error_code: Optional[str] = None, cause: Optional[Exception] = None):
|
|
23
|
+
"""Initialize the installation error.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
message (str): Human-readable error message.
|
|
27
|
+
dependency_name (str, optional): Name of the dependency that failed.
|
|
28
|
+
error_code (str, optional): Machine-readable error code.
|
|
29
|
+
cause (Exception, optional): Underlying exception that caused this error.
|
|
30
|
+
"""
|
|
31
|
+
self.message = message
|
|
32
|
+
self.dependency_name = dependency_name
|
|
33
|
+
self.error_code = error_code
|
|
34
|
+
self.cause = cause
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DependencyInstaller(ABC):
|
|
38
|
+
"""Abstract base class for dependency installers.
|
|
39
|
+
|
|
40
|
+
This class defines the core interface that all concrete installers must implement.
|
|
41
|
+
It provides a consistent API for installing and managing dependencies across
|
|
42
|
+
different types (Hatch packages, Python packages, system packages, Docker containers).
|
|
43
|
+
|
|
44
|
+
The installer design follows these principles:
|
|
45
|
+
- Single responsibility: Each installer handles one dependency type
|
|
46
|
+
- Extensibility: New dependency types can be added by implementing this interface
|
|
47
|
+
- Observability: Progress reporting through callbacks
|
|
48
|
+
- Error handling: Structured exceptions and rollback support
|
|
49
|
+
- Testability: Clear interface for mocking and testing
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def installer_type(self) -> str:
|
|
55
|
+
"""Get the type identifier for this installer.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
str: Unique identifier for the installer type (e.g., "hatch", "python", "docker").
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def supported_schemes(self) -> List[str]:
|
|
65
|
+
"""Get the URI schemes this installer can handle.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List[str]: List of URI schemes (e.g., ["file", "http", "https"] for local/remote packages).
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def can_install(self, dependency: Dict[str, Any]) -> bool:
|
|
74
|
+
"""Check if this installer can handle the given dependency.
|
|
75
|
+
|
|
76
|
+
This method allows the installer registry to determine which installer
|
|
77
|
+
should be used for a specific dependency.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
dependency (Dict[str, Any]): Dependency object with keys like 'type', 'name', 'uri', etc.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
bool: True if this installer can handle the dependency, False otherwise.
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def install(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
89
|
+
progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
|
|
90
|
+
"""Install a dependency.
|
|
91
|
+
|
|
92
|
+
This is the core method that performs the actual installation of a dependency
|
|
93
|
+
into the specified environment.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
dependency (Dict[str, Any]): Dependency object containing:
|
|
97
|
+
- name (str): Name of the dependency
|
|
98
|
+
- version_constraint (str): Version constraint
|
|
99
|
+
- resolved_version (str): Specific version to install
|
|
100
|
+
- uri (str, optional): Download/source URI
|
|
101
|
+
- type (str): Dependency type
|
|
102
|
+
- Additional installer-specific fields
|
|
103
|
+
context (InstallationContext): Installation context with environment info
|
|
104
|
+
progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
|
|
105
|
+
Parameters: (operation_name, progress_percentage, status_message)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
InstallationResult: Result of the installation operation.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
InstallationError: If installation fails for any reason.
|
|
112
|
+
"""
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def uninstall(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
116
|
+
progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
|
|
117
|
+
"""Uninstall a dependency.
|
|
118
|
+
|
|
119
|
+
Default implementation raises NotImplementedError. Concrete installers
|
|
120
|
+
can override this method to provide uninstall functionality.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
dependency (Dict[str, Any]): Dependency object to uninstall.
|
|
124
|
+
context (InstallationContext): Installation context with environment info.
|
|
125
|
+
progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
InstallationResult: Result of the uninstall operation.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
NotImplementedError: If uninstall is not supported by this installer.
|
|
132
|
+
InstallationError: If uninstall fails for any reason.
|
|
133
|
+
"""
|
|
134
|
+
raise NotImplementedError(f"Uninstall not implemented for {self.installer_type} installer")
|
|
135
|
+
|
|
136
|
+
def validate_dependency(self, dependency: Dict[str, Any]) -> bool:
|
|
137
|
+
"""Validate that a dependency object has required fields.
|
|
138
|
+
|
|
139
|
+
This method can be overridden by concrete installers to perform
|
|
140
|
+
installer-specific validation.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
dependency (Dict[str, Any]): Dependency object to validate.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
bool: True if dependency is valid, False otherwise.
|
|
147
|
+
"""
|
|
148
|
+
required_fields = ["name", "version_constraint", "resolved_version"]
|
|
149
|
+
return all(field in dependency for field in required_fields)
|
|
150
|
+
|
|
151
|
+
def get_installation_info(self, dependency: Dict[str, Any], context: InstallationContext) -> Dict[str, Any]:
|
|
152
|
+
"""Get information about what would be installed without actually installing.
|
|
153
|
+
|
|
154
|
+
This method can be used for dry-run scenarios or to provide installation
|
|
155
|
+
previews to users.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
dependency (Dict[str, Any]): Dependency object to analyze.
|
|
159
|
+
context (InstallationContext): Installation context.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dict[str, Any]: Information about the planned installation.
|
|
163
|
+
"""
|
|
164
|
+
return {
|
|
165
|
+
"installer_type": self.installer_type,
|
|
166
|
+
"dependency_name": dependency.get("name"),
|
|
167
|
+
"resolved_version": dependency.get("resolved_version"),
|
|
168
|
+
"target_path": str(context.environment_path),
|
|
169
|
+
"supported": self.can_install(dependency)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def cleanup_failed_installation(self, dependency: Dict[str, Any], context: InstallationContext,
|
|
173
|
+
artifacts: Optional[List[Path]] = None) -> None:
|
|
174
|
+
"""Clean up artifacts from a failed installation.
|
|
175
|
+
|
|
176
|
+
This method is called when an installation fails and needs to be rolled back.
|
|
177
|
+
Concrete installers can override this to perform specific cleanup operations.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
dependency (Dict[str, Any]): Dependency that failed to install.
|
|
181
|
+
context (InstallationContext): Installation context.
|
|
182
|
+
artifacts (List[Path], optional): List of files/directories to clean up.
|
|
183
|
+
"""
|
|
184
|
+
if artifacts:
|
|
185
|
+
for artifact in artifacts:
|
|
186
|
+
try:
|
|
187
|
+
if artifact.exists():
|
|
188
|
+
if artifact.is_file():
|
|
189
|
+
artifact.unlink()
|
|
190
|
+
elif artifact.is_dir():
|
|
191
|
+
import shutil
|
|
192
|
+
shutil.rmtree(artifact)
|
|
193
|
+
except Exception:
|
|
194
|
+
# Log but don't raise - cleanup is best effort
|
|
195
|
+
pass
|