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.
Files changed (93) hide show
  1. hatch/__init__.py +21 -0
  2. hatch/cli_hatch.py +2748 -0
  3. hatch/environment_manager.py +1375 -0
  4. hatch/installers/__init__.py +25 -0
  5. hatch/installers/dependency_installation_orchestrator.py +636 -0
  6. hatch/installers/docker_installer.py +545 -0
  7. hatch/installers/hatch_installer.py +198 -0
  8. hatch/installers/installation_context.py +109 -0
  9. hatch/installers/installer_base.py +195 -0
  10. hatch/installers/python_installer.py +342 -0
  11. hatch/installers/registry.py +179 -0
  12. hatch/installers/system_installer.py +588 -0
  13. hatch/mcp_host_config/__init__.py +38 -0
  14. hatch/mcp_host_config/backup.py +458 -0
  15. hatch/mcp_host_config/host_management.py +572 -0
  16. hatch/mcp_host_config/models.py +602 -0
  17. hatch/mcp_host_config/reporting.py +181 -0
  18. hatch/mcp_host_config/strategies.py +513 -0
  19. hatch/package_loader.py +263 -0
  20. hatch/python_environment_manager.py +734 -0
  21. hatch/registry_explorer.py +171 -0
  22. hatch/registry_retriever.py +335 -0
  23. hatch/template_generator.py +179 -0
  24. hatch_xclam-0.7.0.dist-info/METADATA +150 -0
  25. hatch_xclam-0.7.0.dist-info/RECORD +93 -0
  26. hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
  27. hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
  28. hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
  29. hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
  30. tests/__init__.py +1 -0
  31. tests/run_environment_tests.py +124 -0
  32. tests/test_cli_version.py +122 -0
  33. tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
  34. tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
  35. tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
  36. tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
  37. tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
  38. tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
  39. tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
  40. tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
  41. tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
  42. tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
  43. tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
  44. tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
  45. tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
  46. tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
  47. tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
  48. tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
  49. tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
  50. tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
  51. tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
  52. tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
  53. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
  54. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
  55. tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
  56. tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
  57. tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
  58. tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
  59. tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
  60. tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
  61. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
  62. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
  63. tests/test_data_utils.py +472 -0
  64. tests/test_dependency_orchestrator_consent.py +266 -0
  65. tests/test_docker_installer.py +524 -0
  66. tests/test_env_manip.py +991 -0
  67. tests/test_hatch_installer.py +179 -0
  68. tests/test_installer_base.py +221 -0
  69. tests/test_mcp_atomic_operations.py +276 -0
  70. tests/test_mcp_backup_integration.py +308 -0
  71. tests/test_mcp_cli_all_host_specific_args.py +303 -0
  72. tests/test_mcp_cli_backup_management.py +295 -0
  73. tests/test_mcp_cli_direct_management.py +453 -0
  74. tests/test_mcp_cli_discovery_listing.py +582 -0
  75. tests/test_mcp_cli_host_config_integration.py +823 -0
  76. tests/test_mcp_cli_package_management.py +360 -0
  77. tests/test_mcp_cli_partial_updates.py +859 -0
  78. tests/test_mcp_environment_integration.py +520 -0
  79. tests/test_mcp_host_config_backup.py +257 -0
  80. tests/test_mcp_host_configuration_manager.py +331 -0
  81. tests/test_mcp_host_registry_decorator.py +348 -0
  82. tests/test_mcp_pydantic_architecture_v4.py +603 -0
  83. tests/test_mcp_server_config_models.py +242 -0
  84. tests/test_mcp_server_config_type_field.py +221 -0
  85. tests/test_mcp_sync_functionality.py +316 -0
  86. tests/test_mcp_user_feedback_reporting.py +359 -0
  87. tests/test_non_tty_integration.py +281 -0
  88. tests/test_online_package_loader.py +202 -0
  89. tests/test_python_environment_manager.py +882 -0
  90. tests/test_python_installer.py +327 -0
  91. tests/test_registry.py +51 -0
  92. tests/test_registry_retriever.py +250 -0
  93. 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