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
hatch/package_loader.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Package loader for Hatch.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to download, cache, and install Hatch packages
|
|
4
|
+
from various sources to designated target directories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
import requests
|
|
11
|
+
import zipfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PackageLoaderError(Exception):
|
|
17
|
+
"""Exception raised for package loading errors."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HatchPackageLoader:
|
|
22
|
+
"""Manages the downloading, caching, and installation of Hatch packages."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, cache_dir: Optional[Path] = None):
|
|
25
|
+
"""Initialize the Hatch package loader.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
cache_dir (Path, optional): Directory to store cached files for Hatch.
|
|
29
|
+
Packages will be stored at <cache_dir>/packages.
|
|
30
|
+
Defaults to ~/.hatch/packages.
|
|
31
|
+
"""
|
|
32
|
+
self.logger = logging.getLogger("hatch.package_loader")
|
|
33
|
+
self.logger.setLevel(logging.INFO)
|
|
34
|
+
|
|
35
|
+
# Set up cache directory
|
|
36
|
+
if cache_dir is None:
|
|
37
|
+
cache_dir = Path.home() / '.hatch'
|
|
38
|
+
self.cache_dir = cache_dir / "packages"
|
|
39
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
def _get_package_path(self, package_name: str, version: str) -> Optional[Path]:
|
|
42
|
+
"""Get path to a cached package, if it exists.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
package_name (str): Name of the package.
|
|
46
|
+
version (str): Version of the package.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Optional[Path]: Path to cached package or None if not cached.
|
|
50
|
+
"""
|
|
51
|
+
pkg_path = self.cache_dir / f"{package_name}-{version}"
|
|
52
|
+
if pkg_path.exists() and pkg_path.is_dir():
|
|
53
|
+
return pkg_path
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def download_package(self, package_url: str, package_name: str, version: str, force_download: bool = False) -> Path:
|
|
57
|
+
"""Download a package from a URL and cache it.
|
|
58
|
+
|
|
59
|
+
This method handles the complete download process including:
|
|
60
|
+
1. Checking if the package is already cached
|
|
61
|
+
2. Creating a temporary directory for download
|
|
62
|
+
3. Downloading the package from the URL
|
|
63
|
+
4. Extracting the zip file
|
|
64
|
+
5. Validating the package structure
|
|
65
|
+
6. Moving the package to the cache directory
|
|
66
|
+
|
|
67
|
+
When force_download is True, the method will always download the package directly
|
|
68
|
+
from the source, even if it's already cached. This is useful when you want to ensure
|
|
69
|
+
you have the latest version of a package. When used with registry refresh, it ensures
|
|
70
|
+
both the package metadata and the actual package content are up to date.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
package_url (str): URL to download the package from.
|
|
74
|
+
package_name (str): Name of the package.
|
|
75
|
+
version (str): Version of the package.
|
|
76
|
+
force_download (bool, optional): Force download even if package is cached. Defaults to False.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Path: Path to the downloaded package directory.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
PackageLoaderError: If download or extraction fails.
|
|
83
|
+
"""
|
|
84
|
+
# Check if already cached
|
|
85
|
+
cached_path = self._get_package_path(package_name, version)
|
|
86
|
+
if cached_path and not force_download:
|
|
87
|
+
self.logger.info(f"Using cached package {package_name} v{version}")
|
|
88
|
+
return cached_path
|
|
89
|
+
|
|
90
|
+
if cached_path and force_download:
|
|
91
|
+
self.logger.info(f"Force download requested. Downloading {package_name} v{version} from {package_url}")
|
|
92
|
+
|
|
93
|
+
# Create temporary directory for download
|
|
94
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
95
|
+
temp_dir_path = Path(temp_dir)
|
|
96
|
+
temp_file = temp_dir_path / f"{package_name}-{version}.zip"
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Download the package
|
|
100
|
+
self.logger.info(f"Downloading package from {package_url}")
|
|
101
|
+
# Remote URL - download using requests
|
|
102
|
+
response = requests.get(package_url, stream=True, timeout=30)
|
|
103
|
+
response.raise_for_status()
|
|
104
|
+
|
|
105
|
+
with open(temp_file, 'wb') as f:
|
|
106
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
107
|
+
f.write(chunk)
|
|
108
|
+
|
|
109
|
+
# Extract the package
|
|
110
|
+
extract_dir = temp_dir_path / f"{package_name}-{version}"
|
|
111
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
|
|
113
|
+
with zipfile.ZipFile(temp_file, 'r') as zip_ref:
|
|
114
|
+
zip_ref.extractall(extract_dir)
|
|
115
|
+
|
|
116
|
+
# Ensure expected package structure
|
|
117
|
+
if not (extract_dir / "hatch_metadata.json").exists():
|
|
118
|
+
# Check if the package has a top-level directory
|
|
119
|
+
subdirs = [d for d in extract_dir.iterdir() if d.is_dir()]
|
|
120
|
+
if len(subdirs) == 1 and (subdirs[0] / "hatch_metadata.json").exists():
|
|
121
|
+
# Use the top-level directory as the package
|
|
122
|
+
extract_dir = subdirs[0]
|
|
123
|
+
else:
|
|
124
|
+
raise PackageLoaderError(f"Invalid package structure: hatch_metadata.json not found")
|
|
125
|
+
|
|
126
|
+
# Create the cache directory
|
|
127
|
+
cache_package_dir = self.cache_dir / f"{package_name}-{version}"
|
|
128
|
+
if cache_package_dir.exists():
|
|
129
|
+
shutil.rmtree(cache_package_dir)
|
|
130
|
+
|
|
131
|
+
# Move to cache
|
|
132
|
+
shutil.copytree(extract_dir, cache_package_dir)
|
|
133
|
+
self.logger.info(f"Cached package {package_name} v{version} to {cache_package_dir}")
|
|
134
|
+
|
|
135
|
+
return cache_package_dir
|
|
136
|
+
|
|
137
|
+
except requests.RequestException as e:
|
|
138
|
+
raise PackageLoaderError(f"Failed to download package: {e}")
|
|
139
|
+
except zipfile.BadZipFile:
|
|
140
|
+
raise PackageLoaderError("Downloaded file is not a valid zip archive")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise PackageLoaderError(f"Error downloading package: {e}")
|
|
143
|
+
|
|
144
|
+
def copy_package(self, source_path: Path, target_path: Path) -> bool:
|
|
145
|
+
"""Copy a package from source to target directory.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
source_path (Path): Source directory path.
|
|
149
|
+
target_path (Path): Target directory path.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
bool: True if successful.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
PackageLoaderError: If copy fails.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
if target_path.exists():
|
|
159
|
+
shutil.rmtree(target_path)
|
|
160
|
+
|
|
161
|
+
shutil.copytree(source_path, target_path)
|
|
162
|
+
return True
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise PackageLoaderError(f"Failed to copy package: {e}")
|
|
165
|
+
|
|
166
|
+
def install_local_package(self, source_path: Path, target_dir: Path, package_name: str) -> Path:
|
|
167
|
+
"""Install a local package to the target directory.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
source_path (Path): Path to the source package directory.
|
|
171
|
+
target_dir (Path): Directory to install the package to.
|
|
172
|
+
package_name (str): Name of the package for the target directory.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Path: Path to the installed package.
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
PackageLoaderError: If installation fails.
|
|
179
|
+
"""
|
|
180
|
+
target_path = target_dir / package_name
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
self.copy_package(source_path, target_path)
|
|
184
|
+
self.logger.info(f"Installed local package: {package_name} to {target_path}")
|
|
185
|
+
return target_path
|
|
186
|
+
except Exception as e:
|
|
187
|
+
raise PackageLoaderError(f"Failed to install local package: {e}")
|
|
188
|
+
|
|
189
|
+
def install_remote_package(self, package_url: str, package_name: str,
|
|
190
|
+
version: str, target_dir: Path, force_download: bool = False) -> Path:
|
|
191
|
+
"""Download and install a remote package.
|
|
192
|
+
|
|
193
|
+
This method handles downloading a package from a remote URL and installing it
|
|
194
|
+
into the specified target directory. It leverages the download_package method
|
|
195
|
+
which includes caching functionality, but allows forcing a fresh download when needed.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
package_url (str): URL to download the package from.
|
|
199
|
+
package_name (str): Name of the package.
|
|
200
|
+
version (str): Version of the package.
|
|
201
|
+
target_dir (Path): Directory to install the package to.
|
|
202
|
+
force_download (bool, optional): Force download even if package is cached. Defaults to False.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Path: Path to the installed package.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
PackageLoaderError: If installation fails.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
cached_path = self.download_package(package_url, package_name, version, force_download)
|
|
213
|
+
# Install from cache to target dir
|
|
214
|
+
target_path = target_dir / package_name
|
|
215
|
+
|
|
216
|
+
# Remove existing installation if it exists
|
|
217
|
+
if target_path.exists():
|
|
218
|
+
self.logger.info(f"Removing existing package at {target_path}")
|
|
219
|
+
shutil.rmtree(target_path)
|
|
220
|
+
|
|
221
|
+
# Copy package to target
|
|
222
|
+
self.copy_package(cached_path, target_path)
|
|
223
|
+
|
|
224
|
+
self.logger.info(f"Successfully installed package {package_name} v{version} to {target_path}")
|
|
225
|
+
return target_path
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
raise PackageLoaderError(f"Failed to install remote package {package_name} from {package_url}: {e}")
|
|
229
|
+
|
|
230
|
+
def clear_cache(self, package_name: Optional[str] = None, version: Optional[str] = None) -> bool:
|
|
231
|
+
"""Clear the package cache.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
package_name (str, optional): Name of specific package to clear. Defaults to None (all packages).
|
|
235
|
+
version (str, optional): Version of specific package to clear. Defaults to None (all versions).
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
bool: True if successful.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
if package_name and version:
|
|
242
|
+
# Clear specific package version
|
|
243
|
+
cache_path = self.cache_dir / f"{package_name}-{version}"
|
|
244
|
+
if cache_path.exists():
|
|
245
|
+
shutil.rmtree(cache_path)
|
|
246
|
+
self.logger.info(f"Cleared cache for {package_name}@{version}")
|
|
247
|
+
elif package_name:
|
|
248
|
+
# Clear all versions of specific package
|
|
249
|
+
for path in self.cache_dir.glob(f"{package_name}-*"):
|
|
250
|
+
if path.is_dir():
|
|
251
|
+
shutil.rmtree(path)
|
|
252
|
+
self.logger.info(f"Cleared cache for all versions of {package_name}")
|
|
253
|
+
else:
|
|
254
|
+
# Clear all packages
|
|
255
|
+
for path in self.cache_dir.iterdir():
|
|
256
|
+
if path.is_dir():
|
|
257
|
+
shutil.rmtree(path)
|
|
258
|
+
self.logger.info("Cleared entire package cache")
|
|
259
|
+
|
|
260
|
+
return True
|
|
261
|
+
except Exception as e:
|
|
262
|
+
self.logger.error(f"Failed to clear cache: {e}")
|
|
263
|
+
return False
|