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