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