exonware-xwlazy 0.1.0.1__py3-none-any.whl → 0.1.0.8__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.
- exonware/xwlazy/__init__.py +0 -0
- exonware/xwlazy/version.py +2 -2
- exonware_xwlazy-0.1.0.8.dist-info/METADATA +0 -0
- exonware_xwlazy-0.1.0.8.dist-info/RECORD +6 -0
- exonware/__init__.py +0 -42
- exonware/xwlazy/common/__init__.py +0 -55
- exonware/xwlazy/common/base.py +0 -65
- exonware/xwlazy/common/cache.py +0 -504
- exonware/xwlazy/common/logger.py +0 -257
- exonware/xwlazy/common/services/__init__.py +0 -72
- exonware/xwlazy/common/services/dependency_mapper.py +0 -250
- exonware/xwlazy/common/services/install_async_utils.py +0 -170
- exonware/xwlazy/common/services/install_cache_utils.py +0 -245
- exonware/xwlazy/common/services/keyword_detection.py +0 -283
- exonware/xwlazy/common/services/spec_cache.py +0 -165
- exonware/xwlazy/common/services/state_manager.py +0 -84
- exonware/xwlazy/common/strategies/__init__.py +0 -28
- exonware/xwlazy/common/strategies/caching_dict.py +0 -44
- exonware/xwlazy/common/strategies/caching_installation.py +0 -88
- exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
- exonware/xwlazy/common/strategies/caching_lru.py +0 -63
- exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
- exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
- exonware/xwlazy/common/utils.py +0 -142
- exonware/xwlazy/config.py +0 -193
- exonware/xwlazy/contracts.py +0 -1533
- exonware/xwlazy/defs.py +0 -378
- exonware/xwlazy/errors.py +0 -276
- exonware/xwlazy/facade.py +0 -1137
- exonware/xwlazy/host/__init__.py +0 -8
- exonware/xwlazy/host/conf.py +0 -16
- exonware/xwlazy/module/__init__.py +0 -18
- exonware/xwlazy/module/base.py +0 -643
- exonware/xwlazy/module/data.py +0 -17
- exonware/xwlazy/module/facade.py +0 -246
- exonware/xwlazy/module/importer_engine.py +0 -2964
- exonware/xwlazy/module/partial_module_detector.py +0 -275
- exonware/xwlazy/module/strategies/__init__.py +0 -22
- exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
- exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
- exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
- exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
- exonware/xwlazy/package/__init__.py +0 -18
- exonware/xwlazy/package/base.py +0 -877
- exonware/xwlazy/package/conf.py +0 -324
- exonware/xwlazy/package/data.py +0 -17
- exonware/xwlazy/package/facade.py +0 -480
- exonware/xwlazy/package/services/__init__.py +0 -84
- exonware/xwlazy/package/services/async_install_handle.py +0 -87
- exonware/xwlazy/package/services/config_manager.py +0 -249
- exonware/xwlazy/package/services/discovery.py +0 -435
- exonware/xwlazy/package/services/host_packages.py +0 -180
- exonware/xwlazy/package/services/install_async.py +0 -291
- exonware/xwlazy/package/services/install_cache.py +0 -145
- exonware/xwlazy/package/services/install_interactive.py +0 -59
- exonware/xwlazy/package/services/install_policy.py +0 -156
- exonware/xwlazy/package/services/install_registry.py +0 -54
- exonware/xwlazy/package/services/install_result.py +0 -17
- exonware/xwlazy/package/services/install_sbom.py +0 -153
- exonware/xwlazy/package/services/install_utils.py +0 -79
- exonware/xwlazy/package/services/installer_engine.py +0 -406
- exonware/xwlazy/package/services/lazy_installer.py +0 -803
- exonware/xwlazy/package/services/manifest.py +0 -503
- exonware/xwlazy/package/services/strategy_registry.py +0 -324
- exonware/xwlazy/package/strategies/__init__.py +0 -57
- exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
- exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
- exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
- exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
- exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
- exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
- exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
- exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
- exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
- exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
- exonware/xwlazy/runtime/__init__.py +0 -18
- exonware/xwlazy/runtime/adaptive_learner.py +0 -129
- exonware/xwlazy/runtime/base.py +0 -274
- exonware/xwlazy/runtime/facade.py +0 -94
- exonware/xwlazy/runtime/intelligent_selector.py +0 -170
- exonware/xwlazy/runtime/metrics.py +0 -60
- exonware/xwlazy/runtime/performance.py +0 -37
- exonware_xwlazy-0.1.0.1.dist-info/METADATA +0 -454
- exonware_xwlazy-0.1.0.1.dist-info/RECORD +0 -93
- xwlazy/__init__.py +0 -14
- xwlazy/lazy.py +0 -30
- {exonware_xwlazy-0.1.0.1.dist-info → exonware_xwlazy-0.1.0.8.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.1.dist-info → exonware_xwlazy-0.1.0.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Installation Async Utilities
|
|
3
|
-
|
|
4
|
-
Company: eXonware.com
|
|
5
|
-
Author: Eng. Muhammad AlShehri
|
|
6
|
-
Email: connect@exonware.com
|
|
7
|
-
|
|
8
|
-
Generation Date: 15-Nov-2025
|
|
9
|
-
|
|
10
|
-
Shared utilities for async installation operations.
|
|
11
|
-
Used by both execution strategies and LazyInstaller.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import os
|
|
15
|
-
import sys
|
|
16
|
-
import json
|
|
17
|
-
import asyncio
|
|
18
|
-
import subprocess
|
|
19
|
-
from typing import Optional
|
|
20
|
-
|
|
21
|
-
# Lazy imports
|
|
22
|
-
def _get_logger():
|
|
23
|
-
"""Get logger (lazy import to avoid circular dependency)."""
|
|
24
|
-
from ..logger import get_logger
|
|
25
|
-
return get_logger("xwlazy.install_async_utils")
|
|
26
|
-
|
|
27
|
-
logger = None
|
|
28
|
-
|
|
29
|
-
def _ensure_logging_initialized():
|
|
30
|
-
"""Ensure logging utilities are initialized."""
|
|
31
|
-
global logger
|
|
32
|
-
if logger is None:
|
|
33
|
-
logger = _get_logger()
|
|
34
|
-
# Fallback if get_logger returns None (should not happen but safety first)
|
|
35
|
-
if logger is None:
|
|
36
|
-
import logging
|
|
37
|
-
logger = logging.getLogger("xwlazy.install_async_utils.fallback")
|
|
38
|
-
logger.addHandler(logging.NullHandler())
|
|
39
|
-
|
|
40
|
-
async def get_package_size_mb(package_name: str) -> Optional[float]:
|
|
41
|
-
"""
|
|
42
|
-
Get package size in MB by checking pip show or download size.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
package_name: Package name to check
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
Size in MB or None if cannot determine
|
|
49
|
-
"""
|
|
50
|
-
try:
|
|
51
|
-
process = await asyncio.create_subprocess_exec(
|
|
52
|
-
sys.executable, '-m', 'pip', 'show', package_name,
|
|
53
|
-
stdout=asyncio.subprocess.PIPE,
|
|
54
|
-
stderr=asyncio.subprocess.PIPE
|
|
55
|
-
)
|
|
56
|
-
stdout, _ = await process.communicate()
|
|
57
|
-
|
|
58
|
-
if process.returncode == 0:
|
|
59
|
-
output = stdout.decode()
|
|
60
|
-
for line in output.split('\n'):
|
|
61
|
-
if line.startswith('Location:'):
|
|
62
|
-
location = line.split(':', 1)[1].strip()
|
|
63
|
-
try:
|
|
64
|
-
total_size = 0
|
|
65
|
-
for dirpath, dirnames, filenames in os.walk(location):
|
|
66
|
-
for filename in filenames:
|
|
67
|
-
filepath = os.path.join(dirpath, filename)
|
|
68
|
-
if os.path.exists(filepath):
|
|
69
|
-
total_size += os.path.getsize(filepath)
|
|
70
|
-
return total_size / (1024 * 1024)
|
|
71
|
-
except Exception:
|
|
72
|
-
pass
|
|
73
|
-
except Exception:
|
|
74
|
-
pass
|
|
75
|
-
|
|
76
|
-
# Fallback: Try to get download size from PyPI
|
|
77
|
-
try:
|
|
78
|
-
import urllib.request
|
|
79
|
-
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
80
|
-
with urllib.request.urlopen(url, timeout=5) as response:
|
|
81
|
-
data = json.loads(response.read())
|
|
82
|
-
if 'urls' in data and data['urls']:
|
|
83
|
-
latest = data['urls'][0]
|
|
84
|
-
if 'size' in latest:
|
|
85
|
-
return latest['size'] / (1024 * 1024)
|
|
86
|
-
except Exception:
|
|
87
|
-
pass
|
|
88
|
-
|
|
89
|
-
return None
|
|
90
|
-
|
|
91
|
-
async def async_install_package(
|
|
92
|
-
package_name: str,
|
|
93
|
-
policy_args: Optional[list] = None
|
|
94
|
-
) -> tuple[bool, Optional[str]]:
|
|
95
|
-
"""
|
|
96
|
-
Install a package asynchronously using asyncio subprocess.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
package_name: Package name to install
|
|
100
|
-
policy_args: Optional policy arguments (index URLs, trusted hosts, etc.)
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
Tuple of (success: bool, error_message: Optional[str])
|
|
104
|
-
"""
|
|
105
|
-
_ensure_logging_initialized()
|
|
106
|
-
try:
|
|
107
|
-
pip_args = [sys.executable, '-m', 'pip', 'install']
|
|
108
|
-
if policy_args:
|
|
109
|
-
pip_args.extend(policy_args)
|
|
110
|
-
pip_args.append(package_name)
|
|
111
|
-
|
|
112
|
-
process = await asyncio.create_subprocess_exec(
|
|
113
|
-
*pip_args,
|
|
114
|
-
stdout=asyncio.subprocess.PIPE,
|
|
115
|
-
stderr=asyncio.subprocess.PIPE
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
stdout, stderr = await process.communicate()
|
|
119
|
-
|
|
120
|
-
if process.returncode == 0:
|
|
121
|
-
return True, None
|
|
122
|
-
else:
|
|
123
|
-
error_msg = stderr.decode() if stderr else "Unknown error"
|
|
124
|
-
logger.error(f"Failed to install {package_name}: {error_msg}")
|
|
125
|
-
return False, error_msg
|
|
126
|
-
except Exception as e:
|
|
127
|
-
logger.error(f"Error in async install of {package_name}: {e}")
|
|
128
|
-
return False, str(e)
|
|
129
|
-
|
|
130
|
-
async def async_uninstall_package(
|
|
131
|
-
package_name: str,
|
|
132
|
-
quiet: bool = True
|
|
133
|
-
) -> bool:
|
|
134
|
-
"""
|
|
135
|
-
Uninstall a package asynchronously.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
package_name: Package name to uninstall
|
|
139
|
-
quiet: If True, suppress output
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
True if successful, False otherwise
|
|
143
|
-
"""
|
|
144
|
-
_ensure_logging_initialized()
|
|
145
|
-
try:
|
|
146
|
-
pip_args = [sys.executable, '-m', 'pip', 'uninstall', '-y', package_name]
|
|
147
|
-
|
|
148
|
-
process = await asyncio.create_subprocess_exec(
|
|
149
|
-
*pip_args,
|
|
150
|
-
stdout=asyncio.subprocess.PIPE if quiet else None,
|
|
151
|
-
stderr=asyncio.subprocess.PIPE if quiet else None
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
await process.communicate()
|
|
155
|
-
|
|
156
|
-
if process.returncode == 0:
|
|
157
|
-
if not quiet:
|
|
158
|
-
logger.info(f"Uninstalled {package_name}")
|
|
159
|
-
return True
|
|
160
|
-
return False
|
|
161
|
-
except Exception as e:
|
|
162
|
-
logger.debug(f"Failed to uninstall {package_name}: {e}")
|
|
163
|
-
return False
|
|
164
|
-
|
|
165
|
-
__all__ = [
|
|
166
|
-
'get_package_size_mb',
|
|
167
|
-
'async_install_package',
|
|
168
|
-
'async_uninstall_package',
|
|
169
|
-
]
|
|
170
|
-
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Installation Cache Utilities
|
|
3
|
-
|
|
4
|
-
Company: eXonware.com
|
|
5
|
-
Author: Eng. Muhammad AlShehri
|
|
6
|
-
Email: connect@exonware.com
|
|
7
|
-
|
|
8
|
-
Generation Date: 15-Nov-2025
|
|
9
|
-
|
|
10
|
-
Shared utilities for cache management (wheels, install trees).
|
|
11
|
-
Used by both execution strategies and LazyInstaller.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import os
|
|
15
|
-
import sys
|
|
16
|
-
import shutil
|
|
17
|
-
import sysconfig
|
|
18
|
-
import tempfile
|
|
19
|
-
import subprocess
|
|
20
|
-
import zipfile
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
from typing import Optional
|
|
23
|
-
from contextlib import suppress
|
|
24
|
-
|
|
25
|
-
# Lazy imports
|
|
26
|
-
def _get_logger():
|
|
27
|
-
"""Get logger (lazy import to avoid circular dependency)."""
|
|
28
|
-
from ..logger import get_logger
|
|
29
|
-
return get_logger("xwlazy.install_cache_utils")
|
|
30
|
-
|
|
31
|
-
logger = None
|
|
32
|
-
|
|
33
|
-
# Environment variables
|
|
34
|
-
_DEFAULT_ASYNC_CACHE_DIR = Path(
|
|
35
|
-
os.environ.get(
|
|
36
|
-
"XWLAZY_ASYNC_CACHE_DIR",
|
|
37
|
-
os.path.join(os.path.expanduser("~"), ".xwlazy", "wheel-cache"),
|
|
38
|
-
)
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
def _ensure_logging_initialized():
|
|
42
|
-
"""Ensure logging utilities are initialized."""
|
|
43
|
-
global logger
|
|
44
|
-
if logger is None:
|
|
45
|
-
logger = _get_logger()
|
|
46
|
-
|
|
47
|
-
def get_default_cache_dir() -> Path:
|
|
48
|
-
"""Get the default cache directory."""
|
|
49
|
-
return _DEFAULT_ASYNC_CACHE_DIR
|
|
50
|
-
|
|
51
|
-
def get_cache_dir(cache_dir: Optional[Path] = None) -> Path:
|
|
52
|
-
"""Get cache directory, creating it if necessary."""
|
|
53
|
-
if cache_dir is None:
|
|
54
|
-
cache_dir = _DEFAULT_ASYNC_CACHE_DIR
|
|
55
|
-
path = Path(cache_dir).expanduser()
|
|
56
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
57
|
-
return path
|
|
58
|
-
|
|
59
|
-
def get_wheel_path(package_name: str, cache_dir: Optional[Path] = None) -> Path:
|
|
60
|
-
"""Get the cached wheel file path for a package."""
|
|
61
|
-
cache = get_cache_dir(cache_dir)
|
|
62
|
-
safe = package_name.replace("/", "_").replace("\\", "_").replace(":", "_")
|
|
63
|
-
return cache / f"{safe}.whl"
|
|
64
|
-
|
|
65
|
-
def get_install_tree_dir(package_name: str, cache_dir: Optional[Path] = None) -> Path:
|
|
66
|
-
"""Get the cached install directory for a package."""
|
|
67
|
-
cache = get_cache_dir(cache_dir)
|
|
68
|
-
safe = package_name.replace("/", "_").replace("\\", "_").replace(":", "_")
|
|
69
|
-
return cache / "installs" / safe
|
|
70
|
-
|
|
71
|
-
def get_site_packages_dir() -> Path:
|
|
72
|
-
"""Get the site-packages directory."""
|
|
73
|
-
purelib = sysconfig.get_paths().get("purelib")
|
|
74
|
-
if not purelib:
|
|
75
|
-
purelib = sysconfig.get_paths().get("platlib", sys.prefix)
|
|
76
|
-
path = Path(purelib)
|
|
77
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
78
|
-
return path
|
|
79
|
-
|
|
80
|
-
def pip_install_from_path(wheel_path: Path, policy_args: Optional[list[str]] = None) -> bool:
|
|
81
|
-
"""Install a wheel file using pip."""
|
|
82
|
-
try:
|
|
83
|
-
pip_args = [
|
|
84
|
-
sys.executable,
|
|
85
|
-
'-m',
|
|
86
|
-
'pip',
|
|
87
|
-
'install',
|
|
88
|
-
'--no-deps',
|
|
89
|
-
'--no-input',
|
|
90
|
-
'--disable-pip-version-check',
|
|
91
|
-
]
|
|
92
|
-
if policy_args:
|
|
93
|
-
pip_args.extend(policy_args)
|
|
94
|
-
pip_args.append(str(wheel_path))
|
|
95
|
-
result = subprocess.run(
|
|
96
|
-
pip_args,
|
|
97
|
-
capture_output=True,
|
|
98
|
-
text=True,
|
|
99
|
-
check=True,
|
|
100
|
-
)
|
|
101
|
-
return result.returncode == 0
|
|
102
|
-
except subprocess.CalledProcessError:
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
def ensure_cached_wheel(
|
|
106
|
-
package_name: str,
|
|
107
|
-
policy_args: Optional[list[str]] = None,
|
|
108
|
-
cache_dir: Optional[Path] = None
|
|
109
|
-
) -> Optional[Path]:
|
|
110
|
-
"""Ensure a wheel is cached, downloading it if necessary."""
|
|
111
|
-
wheel_path = get_wheel_path(package_name, cache_dir)
|
|
112
|
-
if wheel_path.exists():
|
|
113
|
-
return wheel_path
|
|
114
|
-
|
|
115
|
-
cache = get_cache_dir(cache_dir)
|
|
116
|
-
try:
|
|
117
|
-
pip_args = [
|
|
118
|
-
sys.executable,
|
|
119
|
-
'-m',
|
|
120
|
-
'pip',
|
|
121
|
-
'wheel',
|
|
122
|
-
'--no-deps',
|
|
123
|
-
'--disable-pip-version-check',
|
|
124
|
-
]
|
|
125
|
-
if policy_args:
|
|
126
|
-
pip_args.extend(policy_args)
|
|
127
|
-
pip_args.extend(['--wheel-dir', str(cache), package_name])
|
|
128
|
-
result = subprocess.run(
|
|
129
|
-
pip_args,
|
|
130
|
-
capture_output=True,
|
|
131
|
-
text=True,
|
|
132
|
-
check=True,
|
|
133
|
-
)
|
|
134
|
-
if result.returncode != 0:
|
|
135
|
-
return None
|
|
136
|
-
candidates = sorted(cache.glob("*.whl"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
137
|
-
if not candidates:
|
|
138
|
-
return None
|
|
139
|
-
primary = candidates[0]
|
|
140
|
-
if wheel_path.exists():
|
|
141
|
-
with suppress(Exception):
|
|
142
|
-
wheel_path.unlink()
|
|
143
|
-
primary.rename(wheel_path)
|
|
144
|
-
for leftover in candidates[1:]:
|
|
145
|
-
with suppress(Exception):
|
|
146
|
-
leftover.unlink()
|
|
147
|
-
return wheel_path
|
|
148
|
-
except subprocess.CalledProcessError:
|
|
149
|
-
return None
|
|
150
|
-
|
|
151
|
-
def install_from_cached_tree(
|
|
152
|
-
package_name: str,
|
|
153
|
-
cache_dir: Optional[Path] = None
|
|
154
|
-
) -> bool:
|
|
155
|
-
"""Install from a cached install tree."""
|
|
156
|
-
_ensure_logging_initialized()
|
|
157
|
-
src = get_install_tree_dir(package_name, cache_dir)
|
|
158
|
-
if not src.exists() or not any(src.iterdir()):
|
|
159
|
-
return False
|
|
160
|
-
target_root = get_site_packages_dir()
|
|
161
|
-
try:
|
|
162
|
-
for item in src.iterdir():
|
|
163
|
-
dest = target_root / item.name
|
|
164
|
-
if dest.exists():
|
|
165
|
-
if dest.is_dir():
|
|
166
|
-
shutil.rmtree(dest, ignore_errors=True)
|
|
167
|
-
else:
|
|
168
|
-
with suppress(FileNotFoundError):
|
|
169
|
-
dest.unlink()
|
|
170
|
-
if item.is_dir():
|
|
171
|
-
shutil.copytree(item, dest)
|
|
172
|
-
else:
|
|
173
|
-
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
-
shutil.copy2(item, dest)
|
|
175
|
-
return True
|
|
176
|
-
except Exception as exc:
|
|
177
|
-
logger.debug("Cached tree install failed for %s: %s", package_name, exc)
|
|
178
|
-
return False
|
|
179
|
-
|
|
180
|
-
def materialize_cached_tree(
|
|
181
|
-
package_name: str,
|
|
182
|
-
wheel_path: Path,
|
|
183
|
-
cache_dir: Optional[Path] = None
|
|
184
|
-
) -> None:
|
|
185
|
-
"""Materialize a cached install tree from a wheel file."""
|
|
186
|
-
_ensure_logging_initialized()
|
|
187
|
-
if not wheel_path or not wheel_path.exists():
|
|
188
|
-
return
|
|
189
|
-
target_dir = get_install_tree_dir(package_name, cache_dir)
|
|
190
|
-
if target_dir.exists() and any(target_dir.iterdir()):
|
|
191
|
-
return
|
|
192
|
-
parent = target_dir.parent
|
|
193
|
-
parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
-
temp_dir = Path(
|
|
195
|
-
tempfile.mkdtemp(prefix="xwlazy-cache-", dir=str(parent))
|
|
196
|
-
)
|
|
197
|
-
try:
|
|
198
|
-
with zipfile.ZipFile(wheel_path, "r") as archive:
|
|
199
|
-
archive.extractall(temp_dir)
|
|
200
|
-
if target_dir.exists():
|
|
201
|
-
shutil.rmtree(target_dir, ignore_errors=True)
|
|
202
|
-
shutil.move(str(temp_dir), str(target_dir))
|
|
203
|
-
except Exception as exc:
|
|
204
|
-
logger.debug("Failed to materialize cached tree for %s: %s", package_name, exc)
|
|
205
|
-
with suppress(Exception):
|
|
206
|
-
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
207
|
-
else:
|
|
208
|
-
return
|
|
209
|
-
finally:
|
|
210
|
-
if temp_dir.exists():
|
|
211
|
-
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
212
|
-
|
|
213
|
-
def has_cached_install_tree(
|
|
214
|
-
package_name: str,
|
|
215
|
-
cache_dir: Optional[Path] = None
|
|
216
|
-
) -> bool:
|
|
217
|
-
"""Check if a cached install tree exists."""
|
|
218
|
-
target = get_install_tree_dir(package_name, cache_dir)
|
|
219
|
-
return target.exists() and any(target.iterdir())
|
|
220
|
-
|
|
221
|
-
def install_from_cached_wheel(
|
|
222
|
-
package_name: str,
|
|
223
|
-
policy_args: Optional[list[str]] = None,
|
|
224
|
-
cache_dir: Optional[Path] = None
|
|
225
|
-
) -> bool:
|
|
226
|
-
"""Install from a cached wheel file."""
|
|
227
|
-
wheel_path = get_wheel_path(package_name, cache_dir)
|
|
228
|
-
if not wheel_path.exists():
|
|
229
|
-
return False
|
|
230
|
-
return pip_install_from_path(wheel_path, policy_args)
|
|
231
|
-
|
|
232
|
-
__all__ = [
|
|
233
|
-
'get_default_cache_dir',
|
|
234
|
-
'get_cache_dir',
|
|
235
|
-
'get_wheel_path',
|
|
236
|
-
'get_install_tree_dir',
|
|
237
|
-
'get_site_packages_dir',
|
|
238
|
-
'pip_install_from_path',
|
|
239
|
-
'ensure_cached_wheel',
|
|
240
|
-
'install_from_cached_tree',
|
|
241
|
-
'materialize_cached_tree',
|
|
242
|
-
'has_cached_install_tree',
|
|
243
|
-
'install_from_cached_wheel',
|
|
244
|
-
]
|
|
245
|
-
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Keyword-based detection for lazy installation.
|
|
3
|
-
|
|
4
|
-
This module provides functionality to detect packages that opt-in to lazy loading
|
|
5
|
-
by including specific keywords in their metadata.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import os
|
|
9
|
-
import sys
|
|
10
|
-
import threading
|
|
11
|
-
from typing import Optional
|
|
12
|
-
|
|
13
|
-
from ..logger import get_logger
|
|
14
|
-
|
|
15
|
-
logger = get_logger("xwlazy.discovery.keyword")
|
|
16
|
-
|
|
17
|
-
# Global configuration
|
|
18
|
-
_KEYWORD_DETECTION_ENABLED: bool = True
|
|
19
|
-
_KEYWORD_TO_CHECK: str = "xwlazy-enabled"
|
|
20
|
-
_keyword_config_lock = threading.RLock()
|
|
21
|
-
|
|
22
|
-
# Detection cache
|
|
23
|
-
_lazy_detection_cache: dict[str, bool] = {}
|
|
24
|
-
_lazy_detection_lock = threading.RLock()
|
|
25
|
-
|
|
26
|
-
def _check_package_keywords(package_name: Optional[str] = None, keyword: Optional[str] = None) -> bool:
|
|
27
|
-
"""
|
|
28
|
-
Check if any installed package has the specified keyword in its metadata.
|
|
29
|
-
|
|
30
|
-
This allows packages to opt-in to lazy loading by adding:
|
|
31
|
-
[project]
|
|
32
|
-
keywords = ["xwlazy-enabled"]
|
|
33
|
-
|
|
34
|
-
in their pyproject.toml file. The keyword is stored in the package's
|
|
35
|
-
metadata when installed.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
package_name: The package name to check (or None to check all packages)
|
|
39
|
-
keyword: The keyword to look for (default: uses _KEYWORD_TO_CHECK)
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
True if the keyword is found in any relevant package's metadata
|
|
43
|
-
"""
|
|
44
|
-
if not _KEYWORD_DETECTION_ENABLED:
|
|
45
|
-
return False
|
|
46
|
-
|
|
47
|
-
if sys.version_info < (3, 8):
|
|
48
|
-
return False
|
|
49
|
-
|
|
50
|
-
try:
|
|
51
|
-
from importlib import metadata
|
|
52
|
-
except Exception as exc:
|
|
53
|
-
logger.debug(f"importlib.metadata unavailable for keyword detection: {exc}")
|
|
54
|
-
return False
|
|
55
|
-
|
|
56
|
-
with _keyword_config_lock:
|
|
57
|
-
search_keyword = (keyword or _KEYWORD_TO_CHECK).lower()
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
if package_name:
|
|
61
|
-
# Check specific package
|
|
62
|
-
try:
|
|
63
|
-
dist = metadata.distribution(package_name)
|
|
64
|
-
keywords = dist.metadata.get_all('Keywords', [])
|
|
65
|
-
if keywords:
|
|
66
|
-
# Keywords can be a single string or list
|
|
67
|
-
all_keywords = []
|
|
68
|
-
for kw in keywords:
|
|
69
|
-
if isinstance(kw, str):
|
|
70
|
-
# Split comma-separated keywords
|
|
71
|
-
all_keywords.extend(k.strip().lower() for k in kw.split(','))
|
|
72
|
-
else:
|
|
73
|
-
all_keywords.append(str(kw).lower())
|
|
74
|
-
|
|
75
|
-
if search_keyword in all_keywords:
|
|
76
|
-
logger.info(f"✅ Detected '{search_keyword}' keyword in package: {package_name}")
|
|
77
|
-
return True
|
|
78
|
-
except metadata.PackageNotFoundError:
|
|
79
|
-
return False
|
|
80
|
-
else:
|
|
81
|
-
# Check all installed packages
|
|
82
|
-
for dist in metadata.distributions():
|
|
83
|
-
try:
|
|
84
|
-
keywords = dist.metadata.get_all('Keywords', [])
|
|
85
|
-
if keywords:
|
|
86
|
-
all_keywords = []
|
|
87
|
-
for kw in keywords:
|
|
88
|
-
if isinstance(kw, str):
|
|
89
|
-
all_keywords.extend(k.strip().lower() for k in kw.split(','))
|
|
90
|
-
else:
|
|
91
|
-
all_keywords.append(str(kw).lower())
|
|
92
|
-
|
|
93
|
-
if search_keyword in all_keywords:
|
|
94
|
-
package_found = dist.metadata.get('Name', 'unknown')
|
|
95
|
-
logger.info(f"✅ Detected '{search_keyword}' keyword in package: {package_found}")
|
|
96
|
-
return True
|
|
97
|
-
except Exception:
|
|
98
|
-
continue
|
|
99
|
-
except Exception as exc:
|
|
100
|
-
logger.debug(f"Failed to check package keywords: {exc}")
|
|
101
|
-
|
|
102
|
-
return False
|
|
103
|
-
|
|
104
|
-
def _lazy_marker_installed() -> bool:
|
|
105
|
-
"""Check if the exonware-xwlazy marker package is installed."""
|
|
106
|
-
if sys.version_info < (3, 8):
|
|
107
|
-
return False
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
from importlib import metadata
|
|
111
|
-
except Exception as exc:
|
|
112
|
-
logger.debug(f"importlib.metadata unavailable for lazy detection: {exc}")
|
|
113
|
-
return False
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
metadata.distribution("exonware-xwlazy")
|
|
117
|
-
logger.info("✅ Detected exonware-xwlazy marker package")
|
|
118
|
-
return True
|
|
119
|
-
except metadata.PackageNotFoundError:
|
|
120
|
-
logger.debug("❌ exonware-xwlazy marker package not installed")
|
|
121
|
-
return False
|
|
122
|
-
|
|
123
|
-
def _lazy_env_override(package_name: str) -> Optional[bool]:
|
|
124
|
-
"""Check environment variable override for lazy installation."""
|
|
125
|
-
env_var = f"{package_name.upper()}_LAZY_INSTALL"
|
|
126
|
-
raw_value = os.environ.get(env_var)
|
|
127
|
-
if raw_value is None:
|
|
128
|
-
return None
|
|
129
|
-
|
|
130
|
-
normalized = raw_value.strip().lower()
|
|
131
|
-
if normalized in ("true", "1", "yes", "on"):
|
|
132
|
-
return True
|
|
133
|
-
if normalized in ("false", "0", "no", "off"):
|
|
134
|
-
return False
|
|
135
|
-
return None
|
|
136
|
-
|
|
137
|
-
def _detect_meta_info_mode(package_name: str) -> Optional[str]:
|
|
138
|
-
"""
|
|
139
|
-
Detect lazy mode from package metadata keywords.
|
|
140
|
-
|
|
141
|
-
Checks for keywords like:
|
|
142
|
-
- xwlazy-load-install-uninstall (clean mode)
|
|
143
|
-
- xwlazy-lite (lite mode)
|
|
144
|
-
- xwlazy-smart (smart mode)
|
|
145
|
-
- xwlazy-full (full mode)
|
|
146
|
-
- xwlazy-auto (auto mode)
|
|
147
|
-
|
|
148
|
-
Returns:
|
|
149
|
-
Mode string or None if not found
|
|
150
|
-
"""
|
|
151
|
-
try:
|
|
152
|
-
import importlib.metadata
|
|
153
|
-
try:
|
|
154
|
-
dist = importlib.metadata.distribution(package_name)
|
|
155
|
-
keywords = dist.metadata.get_all("Keywords", [])
|
|
156
|
-
if not keywords:
|
|
157
|
-
return None
|
|
158
|
-
|
|
159
|
-
keyword_str = " ".join(keywords).lower()
|
|
160
|
-
|
|
161
|
-
if "xwlazy-load-install-uninstall" in keyword_str:
|
|
162
|
-
return "clean"
|
|
163
|
-
if "xwlazy-lite" in keyword_str:
|
|
164
|
-
return "lite"
|
|
165
|
-
if "xwlazy-smart" in keyword_str:
|
|
166
|
-
return "smart"
|
|
167
|
-
if "xwlazy-full" in keyword_str:
|
|
168
|
-
return "full"
|
|
169
|
-
if "xwlazy-auto" in keyword_str:
|
|
170
|
-
return "auto"
|
|
171
|
-
except importlib.metadata.PackageNotFoundError:
|
|
172
|
-
return None
|
|
173
|
-
except Exception:
|
|
174
|
-
pass
|
|
175
|
-
return None
|
|
176
|
-
|
|
177
|
-
def enable_keyword_detection(enabled: bool = True, keyword: Optional[str] = None) -> None:
|
|
178
|
-
"""
|
|
179
|
-
Enable/disable keyword-based auto-detection of lazy loading.
|
|
180
|
-
|
|
181
|
-
When enabled, xwlazy will check installed packages for a keyword
|
|
182
|
-
(default: "xwlazy-enabled") in their metadata. Packages can opt-in
|
|
183
|
-
by adding the keyword to their pyproject.toml:
|
|
184
|
-
|
|
185
|
-
[project]
|
|
186
|
-
keywords = ["xwlazy-enabled"]
|
|
187
|
-
|
|
188
|
-
Args:
|
|
189
|
-
enabled: Whether to enable keyword detection (default: True)
|
|
190
|
-
keyword: Custom keyword to check (default: "xwlazy-enabled")
|
|
191
|
-
"""
|
|
192
|
-
global _KEYWORD_DETECTION_ENABLED, _KEYWORD_TO_CHECK
|
|
193
|
-
with _keyword_config_lock:
|
|
194
|
-
_KEYWORD_DETECTION_ENABLED = enabled
|
|
195
|
-
if keyword is not None:
|
|
196
|
-
_KEYWORD_TO_CHECK = keyword
|
|
197
|
-
# Clear cache to force re-detection
|
|
198
|
-
with _lazy_detection_lock:
|
|
199
|
-
_lazy_detection_cache.clear()
|
|
200
|
-
|
|
201
|
-
def is_keyword_detection_enabled() -> bool:
|
|
202
|
-
"""Return whether keyword-based detection is enabled."""
|
|
203
|
-
with _keyword_config_lock:
|
|
204
|
-
return _KEYWORD_DETECTION_ENABLED
|
|
205
|
-
|
|
206
|
-
def get_keyword_detection_keyword() -> str:
|
|
207
|
-
"""Get the keyword currently being checked for auto-detection."""
|
|
208
|
-
with _keyword_config_lock:
|
|
209
|
-
return _KEYWORD_TO_CHECK
|
|
210
|
-
|
|
211
|
-
def check_package_keywords(package_name: Optional[str] = None, keyword: Optional[str] = None) -> bool:
|
|
212
|
-
"""
|
|
213
|
-
Check if a package (or any package) has the specified keyword in its metadata.
|
|
214
|
-
|
|
215
|
-
This is the public API for the keyword detection functionality.
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
package_name: The package name to check (or None to check all packages)
|
|
219
|
-
keyword: The keyword to look for (default: uses configured keyword)
|
|
220
|
-
|
|
221
|
-
Returns:
|
|
222
|
-
True if the keyword is found in the package's metadata
|
|
223
|
-
"""
|
|
224
|
-
return _check_package_keywords(package_name, keyword)
|
|
225
|
-
|
|
226
|
-
def _detect_lazy_installation(package_name: str) -> bool:
|
|
227
|
-
"""
|
|
228
|
-
Detect if lazy installation should be enabled for a package.
|
|
229
|
-
|
|
230
|
-
This function checks multiple sources in order:
|
|
231
|
-
1. Environment variable override
|
|
232
|
-
2. Manual state (from state manager)
|
|
233
|
-
3. Cached auto state
|
|
234
|
-
4. Marker package detection
|
|
235
|
-
5. Keyword detection
|
|
236
|
-
|
|
237
|
-
Args:
|
|
238
|
-
package_name: The package name to check
|
|
239
|
-
|
|
240
|
-
Returns:
|
|
241
|
-
True if lazy installation should be enabled
|
|
242
|
-
"""
|
|
243
|
-
with _lazy_detection_lock:
|
|
244
|
-
cached = _lazy_detection_cache.get(package_name)
|
|
245
|
-
if cached is not None:
|
|
246
|
-
return cached
|
|
247
|
-
|
|
248
|
-
env_override = _lazy_env_override(package_name)
|
|
249
|
-
if env_override is not None:
|
|
250
|
-
with _lazy_detection_lock:
|
|
251
|
-
_lazy_detection_cache[package_name] = env_override
|
|
252
|
-
return env_override
|
|
253
|
-
|
|
254
|
-
from .state_manager import LazyStateManager
|
|
255
|
-
state_manager = LazyStateManager(package_name)
|
|
256
|
-
manual_state = state_manager.get_manual_state()
|
|
257
|
-
if manual_state is not None:
|
|
258
|
-
with _lazy_detection_lock:
|
|
259
|
-
_lazy_detection_cache[package_name] = manual_state
|
|
260
|
-
return manual_state
|
|
261
|
-
|
|
262
|
-
cached_state = state_manager.get_cached_auto_state()
|
|
263
|
-
if cached_state is not None:
|
|
264
|
-
with _lazy_detection_lock:
|
|
265
|
-
_lazy_detection_cache[package_name] = cached_state
|
|
266
|
-
return cached_state
|
|
267
|
-
|
|
268
|
-
# Check marker package first (existing behavior)
|
|
269
|
-
marker_detected = _lazy_marker_installed()
|
|
270
|
-
|
|
271
|
-
# Also check for keyword in package metadata (new feature)
|
|
272
|
-
keyword_detected = _check_package_keywords(package_name)
|
|
273
|
-
|
|
274
|
-
# Enable if either marker package OR keyword is found
|
|
275
|
-
detected = marker_detected or keyword_detected
|
|
276
|
-
|
|
277
|
-
state_manager.set_auto_state(detected)
|
|
278
|
-
|
|
279
|
-
with _lazy_detection_lock:
|
|
280
|
-
_lazy_detection_cache[package_name] = detected
|
|
281
|
-
|
|
282
|
-
return detected
|
|
283
|
-
|