exonware-xwlazy 0.1.0.11__py3-none-any.whl → 0.1.0.19__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/__init__.py +22 -0
- exonware/xwlazy/__init__.py +0 -0
- exonware/xwlazy/common/__init__.py +47 -0
- exonware/xwlazy/common/base.py +58 -0
- exonware/xwlazy/common/cache.py +506 -0
- exonware/xwlazy/common/logger.py +268 -0
- exonware/xwlazy/common/services/__init__.py +72 -0
- exonware/xwlazy/common/services/dependency_mapper.py +234 -0
- exonware/xwlazy/common/services/install_async_utils.py +169 -0
- exonware/xwlazy/common/services/install_cache_utils.py +257 -0
- exonware/xwlazy/common/services/keyword_detection.py +292 -0
- exonware/xwlazy/common/services/spec_cache.py +173 -0
- exonware/xwlazy/common/strategies/__init__.py +28 -0
- exonware/xwlazy/common/strategies/caching_dict.py +45 -0
- exonware/xwlazy/common/strategies/caching_installation.py +89 -0
- exonware/xwlazy/common/strategies/caching_lfu.py +67 -0
- exonware/xwlazy/common/strategies/caching_lru.py +64 -0
- exonware/xwlazy/common/strategies/caching_multitier.py +60 -0
- exonware/xwlazy/common/strategies/caching_ttl.py +60 -0
- {xwlazy/lazy → exonware/xwlazy}/config.py +52 -20
- exonware/xwlazy/contracts.py +1410 -0
- exonware/xwlazy/defs.py +397 -0
- xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -8
- exonware/xwlazy/facade.py +1049 -0
- exonware/xwlazy/module/__init__.py +18 -0
- exonware/xwlazy/module/base.py +569 -0
- exonware/xwlazy/module/data.py +17 -0
- exonware/xwlazy/module/facade.py +247 -0
- exonware/xwlazy/module/importer_engine.py +2161 -0
- exonware/xwlazy/module/strategies/__init__.py +22 -0
- exonware/xwlazy/module/strategies/module_helper_lazy.py +94 -0
- exonware/xwlazy/module/strategies/module_helper_simple.py +66 -0
- exonware/xwlazy/module/strategies/module_manager_advanced.py +112 -0
- exonware/xwlazy/module/strategies/module_manager_simple.py +96 -0
- exonware/xwlazy/package/__init__.py +18 -0
- exonware/xwlazy/package/base.py +807 -0
- xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +62 -10
- exonware/xwlazy/package/data.py +17 -0
- exonware/xwlazy/package/facade.py +481 -0
- exonware/xwlazy/package/services/__init__.py +84 -0
- exonware/xwlazy/package/services/async_install_handle.py +89 -0
- exonware/xwlazy/package/services/config_manager.py +246 -0
- exonware/xwlazy/package/services/discovery.py +374 -0
- {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -16
- exonware/xwlazy/package/services/install_async.py +278 -0
- exonware/xwlazy/package/services/install_cache.py +146 -0
- exonware/xwlazy/package/services/install_interactive.py +60 -0
- exonware/xwlazy/package/services/install_policy.py +158 -0
- exonware/xwlazy/package/services/install_registry.py +56 -0
- exonware/xwlazy/package/services/install_result.py +17 -0
- exonware/xwlazy/package/services/install_sbom.py +154 -0
- exonware/xwlazy/package/services/install_utils.py +83 -0
- exonware/xwlazy/package/services/installer_engine.py +408 -0
- exonware/xwlazy/package/services/lazy_installer.py +720 -0
- {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +42 -25
- exonware/xwlazy/package/services/strategy_registry.py +188 -0
- exonware/xwlazy/package/strategies/__init__.py +57 -0
- exonware/xwlazy/package/strategies/package_discovery_file.py +130 -0
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +85 -0
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +102 -0
- exonware/xwlazy/package/strategies/package_execution_async.py +114 -0
- exonware/xwlazy/package/strategies/package_execution_cached.py +91 -0
- exonware/xwlazy/package/strategies/package_execution_pip.py +100 -0
- exonware/xwlazy/package/strategies/package_execution_wheel.py +107 -0
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +101 -0
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +106 -0
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +101 -0
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +58 -0
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +58 -0
- exonware/xwlazy/package/strategies/package_policy_permissive.py +47 -0
- exonware/xwlazy/package/strategies/package_timing_clean.py +68 -0
- exonware/xwlazy/package/strategies/package_timing_full.py +67 -0
- exonware/xwlazy/package/strategies/package_timing_smart.py +69 -0
- exonware/xwlazy/package/strategies/package_timing_temporary.py +67 -0
- exonware/xwlazy/runtime/__init__.py +18 -0
- exonware/xwlazy/runtime/adaptive_learner.py +131 -0
- exonware/xwlazy/runtime/base.py +276 -0
- exonware/xwlazy/runtime/facade.py +95 -0
- exonware/xwlazy/runtime/intelligent_selector.py +173 -0
- exonware/xwlazy/runtime/metrics.py +64 -0
- exonware/xwlazy/runtime/performance.py +39 -0
- exonware/xwlazy/version.py +2 -2
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/METADATA +86 -10
- exonware_xwlazy-0.1.0.19.dist-info/RECORD +87 -0
- exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
- xwlazy/__init__.py +0 -34
- xwlazy/lazy/__init__.py +0 -301
- xwlazy/lazy/bootstrap.py +0 -106
- xwlazy/lazy/lazy_base.py +0 -465
- xwlazy/lazy/lazy_contracts.py +0 -290
- xwlazy/lazy/lazy_core.py +0 -3727
- xwlazy/lazy/logging_utils.py +0 -194
- xwlazy/version.py +0 -77
- /xwlazy/lazy/lazy_state.py → /exonware/xwlazy/common/services/state_manager.py +0 -0
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Installer Engine
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
Version: 0.1.0.19
|
|
8
|
+
Generation Date: 15-Nov-2025
|
|
9
|
+
|
|
10
|
+
Unified async execution engine for install operations.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import asyncio
|
|
16
|
+
import threading
|
|
17
|
+
import importlib.metadata
|
|
18
|
+
from typing import Dict, List, Optional, Set, Callable
|
|
19
|
+
|
|
20
|
+
from .install_result import InstallResult, InstallStatus
|
|
21
|
+
from .install_policy import LazyInstallPolicy
|
|
22
|
+
from ...common.cache import InstallationCache
|
|
23
|
+
from .config_manager import LazyInstallConfig
|
|
24
|
+
from ...defs import LazyInstallMode
|
|
25
|
+
|
|
26
|
+
# Lazy import to avoid circular dependency
|
|
27
|
+
def _get_logger():
|
|
28
|
+
"""Get logger (lazy import to avoid circular dependency)."""
|
|
29
|
+
from ...common.logger import get_logger
|
|
30
|
+
return get_logger("xwlazy.installer_engine")
|
|
31
|
+
|
|
32
|
+
logger = None # Will be initialized on first use
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InstallerEngine:
|
|
36
|
+
"""
|
|
37
|
+
Unified async execution engine for install operations.
|
|
38
|
+
|
|
39
|
+
Features:
|
|
40
|
+
- Parallel async execution for installs (waits for all to complete)
|
|
41
|
+
- Integration with InstallCache and InstallPolicy
|
|
42
|
+
- Support for all install modes (SMART, FULL, CLEAN, TEMPORARY, SIZE_AWARE, etc.)
|
|
43
|
+
- Progress tracking and error handling
|
|
44
|
+
- Retry logic with exponential backoff
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, package_name: str = 'default'):
|
|
48
|
+
"""
|
|
49
|
+
Initialize installer engine.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
package_name: Package name for isolation
|
|
53
|
+
"""
|
|
54
|
+
self._package_name = package_name
|
|
55
|
+
self._install_cache = InstallationCache()
|
|
56
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
57
|
+
self._loop_thread: Optional[threading.Thread] = None
|
|
58
|
+
self._lock = threading.RLock()
|
|
59
|
+
self._active_installs: Set[str] = set() # Track active installs to prevent duplicates
|
|
60
|
+
|
|
61
|
+
# Get install mode from config
|
|
62
|
+
self._mode = LazyInstallConfig.get_install_mode(package_name) or LazyInstallMode.SMART
|
|
63
|
+
|
|
64
|
+
# Initialize logger
|
|
65
|
+
global logger
|
|
66
|
+
if logger is None:
|
|
67
|
+
logger = _get_logger()
|
|
68
|
+
|
|
69
|
+
def _ensure_loop(self) -> asyncio.AbstractEventLoop:
|
|
70
|
+
"""Ensure async event loop is running in background thread."""
|
|
71
|
+
if self._loop is None or self._loop.is_closed():
|
|
72
|
+
self._loop = asyncio.new_event_loop()
|
|
73
|
+
self._loop_thread = threading.Thread(
|
|
74
|
+
target=self._run_loop,
|
|
75
|
+
daemon=True,
|
|
76
|
+
name=f"InstallerEngine-{self._package_name}"
|
|
77
|
+
)
|
|
78
|
+
self._loop_thread.start()
|
|
79
|
+
return self._loop
|
|
80
|
+
|
|
81
|
+
def _run_loop(self):
|
|
82
|
+
"""Run event loop in background thread."""
|
|
83
|
+
asyncio.set_event_loop(self._loop)
|
|
84
|
+
self._loop.run_forever()
|
|
85
|
+
|
|
86
|
+
def _get_installed_version(self, package_name: str) -> Optional[str]:
|
|
87
|
+
"""Get installed version of a package."""
|
|
88
|
+
try:
|
|
89
|
+
dist = importlib.metadata.distribution(package_name)
|
|
90
|
+
return dist.version
|
|
91
|
+
except importlib.metadata.PackageNotFoundError:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
async def _install_single(
|
|
95
|
+
self,
|
|
96
|
+
package_name: str,
|
|
97
|
+
module_name: Optional[str] = None,
|
|
98
|
+
max_retries: int = 3,
|
|
99
|
+
initial_delay: float = 1.0
|
|
100
|
+
) -> InstallResult:
|
|
101
|
+
"""
|
|
102
|
+
Install a single package asynchronously.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
package_name: Package name to install
|
|
106
|
+
module_name: Optional module name (for context)
|
|
107
|
+
max_retries: Maximum retry attempts
|
|
108
|
+
initial_delay: Initial delay before retry (exponential backoff)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
InstallResult with success status and details
|
|
112
|
+
"""
|
|
113
|
+
# Prevent duplicate installs
|
|
114
|
+
with self._lock:
|
|
115
|
+
if package_name in self._active_installs:
|
|
116
|
+
logger.debug(f"Install already in progress for {package_name}, skipping")
|
|
117
|
+
return InstallResult(
|
|
118
|
+
package_name=package_name,
|
|
119
|
+
success=False,
|
|
120
|
+
status=InstallStatus.SKIPPED,
|
|
121
|
+
error="Install already in progress"
|
|
122
|
+
)
|
|
123
|
+
self._active_installs.add(package_name)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Check cache first
|
|
127
|
+
if self._install_cache.is_installed(package_name):
|
|
128
|
+
version = self._install_cache.get_version(package_name)
|
|
129
|
+
logger.debug(f"Package {package_name} already installed (cached)")
|
|
130
|
+
return InstallResult(
|
|
131
|
+
package_name=package_name,
|
|
132
|
+
success=True,
|
|
133
|
+
status=InstallStatus.SUCCESS,
|
|
134
|
+
version=version,
|
|
135
|
+
source="cache"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Security check
|
|
139
|
+
allowed, reason = LazyInstallPolicy.is_package_allowed(
|
|
140
|
+
self._package_name, package_name
|
|
141
|
+
)
|
|
142
|
+
if not allowed:
|
|
143
|
+
return InstallResult(
|
|
144
|
+
package_name=package_name,
|
|
145
|
+
success=False,
|
|
146
|
+
status=InstallStatus.FAILED,
|
|
147
|
+
error=f"Security policy violation: {reason}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Check externally managed environment (PEP 668)
|
|
151
|
+
from .install_utils import is_externally_managed
|
|
152
|
+
if is_externally_managed():
|
|
153
|
+
return InstallResult(
|
|
154
|
+
package_name=package_name,
|
|
155
|
+
success=False,
|
|
156
|
+
status=InstallStatus.FAILED,
|
|
157
|
+
error="Environment is externally managed (PEP 668)"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# SIZE_AWARE mode: Check package size
|
|
161
|
+
if self._mode == LazyInstallMode.SIZE_AWARE:
|
|
162
|
+
mode_config = LazyInstallConfig.get_mode_config(self._package_name)
|
|
163
|
+
threshold_mb = mode_config.large_package_threshold_mb if mode_config else 50.0
|
|
164
|
+
|
|
165
|
+
size_mb = await self._get_package_size_mb(package_name)
|
|
166
|
+
if size_mb is not None and size_mb >= threshold_mb:
|
|
167
|
+
logger.warning(
|
|
168
|
+
f"Package '{package_name}' is {size_mb:.1f}MB "
|
|
169
|
+
f"(>= {threshold_mb}MB threshold), skipping in SIZE_AWARE mode"
|
|
170
|
+
)
|
|
171
|
+
return InstallResult(
|
|
172
|
+
package_name=package_name,
|
|
173
|
+
success=False,
|
|
174
|
+
status=InstallStatus.SKIPPED,
|
|
175
|
+
error=f"Package too large ({size_mb:.1f}MB >= {threshold_mb}MB)"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Retry logic with exponential backoff
|
|
179
|
+
delay = initial_delay
|
|
180
|
+
last_error = None
|
|
181
|
+
|
|
182
|
+
for attempt in range(max_retries):
|
|
183
|
+
try:
|
|
184
|
+
# Get pip args from policy
|
|
185
|
+
policy_args = LazyInstallPolicy.get_pip_args(self._package_name) or []
|
|
186
|
+
pip_args = [
|
|
187
|
+
sys.executable, '-m', 'pip', 'install', package_name
|
|
188
|
+
] + policy_args
|
|
189
|
+
|
|
190
|
+
# Run pip install
|
|
191
|
+
process = await asyncio.create_subprocess_exec(
|
|
192
|
+
*pip_args,
|
|
193
|
+
stdout=asyncio.subprocess.PIPE,
|
|
194
|
+
stderr=asyncio.subprocess.PIPE
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
stdout, stderr = await process.communicate()
|
|
198
|
+
|
|
199
|
+
if process.returncode == 0:
|
|
200
|
+
# Success - mark in cache
|
|
201
|
+
version = self._get_installed_version(package_name)
|
|
202
|
+
self._install_cache.mark_installed(package_name, version)
|
|
203
|
+
|
|
204
|
+
logger.info(f"Successfully installed {package_name} (version: {version})")
|
|
205
|
+
|
|
206
|
+
return InstallResult(
|
|
207
|
+
package_name=package_name,
|
|
208
|
+
success=True,
|
|
209
|
+
status=InstallStatus.SUCCESS,
|
|
210
|
+
version=version,
|
|
211
|
+
source="pip"
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
error_msg = stderr.decode() if stderr else "Unknown error"
|
|
215
|
+
last_error = f"pip install failed: {error_msg}"
|
|
216
|
+
|
|
217
|
+
if attempt < max_retries - 1:
|
|
218
|
+
logger.warning(
|
|
219
|
+
f"Install attempt {attempt + 1} failed for {package_name}, "
|
|
220
|
+
f"retrying in {delay}s..."
|
|
221
|
+
)
|
|
222
|
+
await asyncio.sleep(delay)
|
|
223
|
+
delay *= 2 # Exponential backoff
|
|
224
|
+
else:
|
|
225
|
+
logger.error(f"Failed to install {package_name} after {max_retries} attempts")
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
last_error = str(e)
|
|
229
|
+
if attempt < max_retries - 1:
|
|
230
|
+
logger.warning(
|
|
231
|
+
f"Install attempt {attempt + 1} failed for {package_name}: {e}, "
|
|
232
|
+
f"retrying in {delay}s..."
|
|
233
|
+
)
|
|
234
|
+
await asyncio.sleep(delay)
|
|
235
|
+
delay *= 2
|
|
236
|
+
else:
|
|
237
|
+
logger.error(f"Error installing {package_name}: {e}")
|
|
238
|
+
|
|
239
|
+
return InstallResult(
|
|
240
|
+
package_name=package_name,
|
|
241
|
+
success=False,
|
|
242
|
+
status=InstallStatus.FAILED,
|
|
243
|
+
error=last_error or "Unknown error"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
finally:
|
|
247
|
+
# Remove from active installs
|
|
248
|
+
with self._lock:
|
|
249
|
+
self._active_installs.discard(package_name)
|
|
250
|
+
|
|
251
|
+
async def _get_package_size_mb(self, package_name: str) -> Optional[float]:
|
|
252
|
+
"""Get package size in MB (for SIZE_AWARE mode)."""
|
|
253
|
+
try:
|
|
254
|
+
cmd = [
|
|
255
|
+
sys.executable, '-m', 'pip', 'show', package_name
|
|
256
|
+
]
|
|
257
|
+
process = await asyncio.create_subprocess_exec(
|
|
258
|
+
*cmd,
|
|
259
|
+
stdout=asyncio.subprocess.PIPE,
|
|
260
|
+
stderr=asyncio.subprocess.PIPE
|
|
261
|
+
)
|
|
262
|
+
stdout, _ = await process.communicate()
|
|
263
|
+
|
|
264
|
+
if process.returncode == 0:
|
|
265
|
+
# Parse output to find size
|
|
266
|
+
# This is a simplified version - actual implementation may vary
|
|
267
|
+
output = stdout.decode()
|
|
268
|
+
# Look for Size: field or estimate from download size
|
|
269
|
+
# For now, return None (would need more sophisticated parsing)
|
|
270
|
+
return None
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def install_many(
|
|
276
|
+
self,
|
|
277
|
+
*package_names: str,
|
|
278
|
+
callback: Optional[Callable[[str, InstallResult], None]] = None
|
|
279
|
+
) -> Dict[str, InstallResult]:
|
|
280
|
+
"""
|
|
281
|
+
Install multiple packages in parallel (async), but wait for all to complete.
|
|
282
|
+
|
|
283
|
+
This is a SYNCHRONOUS method that internally uses async execution.
|
|
284
|
+
It waits for all installations to complete before returning.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
*package_names: Package names to install
|
|
288
|
+
callback: Optional callback called as (package_name, result) for each completion
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Dict mapping package_name -> InstallResult
|
|
292
|
+
"""
|
|
293
|
+
if not package_names:
|
|
294
|
+
return {}
|
|
295
|
+
|
|
296
|
+
# Filter out already installed packages
|
|
297
|
+
to_install = []
|
|
298
|
+
results = {}
|
|
299
|
+
|
|
300
|
+
for name in package_names:
|
|
301
|
+
if self._install_cache.is_installed(name):
|
|
302
|
+
version = self._install_cache.get_version(name)
|
|
303
|
+
result = InstallResult(
|
|
304
|
+
package_name=name,
|
|
305
|
+
success=True,
|
|
306
|
+
status=InstallStatus.SUCCESS,
|
|
307
|
+
version=version,
|
|
308
|
+
source="cache"
|
|
309
|
+
)
|
|
310
|
+
results[name] = result
|
|
311
|
+
if callback:
|
|
312
|
+
callback(name, result)
|
|
313
|
+
else:
|
|
314
|
+
to_install.append(name)
|
|
315
|
+
|
|
316
|
+
if not to_install:
|
|
317
|
+
return results
|
|
318
|
+
|
|
319
|
+
# Create async tasks for all packages
|
|
320
|
+
loop = self._ensure_loop()
|
|
321
|
+
|
|
322
|
+
async def _install_all():
|
|
323
|
+
"""Install all packages in parallel."""
|
|
324
|
+
tasks = [
|
|
325
|
+
self._install_single(name)
|
|
326
|
+
for name in to_install
|
|
327
|
+
]
|
|
328
|
+
return await asyncio.gather(*tasks, return_exceptions=True)
|
|
329
|
+
|
|
330
|
+
# Wait for all to complete (synchronous wait)
|
|
331
|
+
try:
|
|
332
|
+
future = asyncio.run_coroutine_threadsafe(_install_all(), loop)
|
|
333
|
+
install_results = future.result(timeout=600) # 10 min timeout
|
|
334
|
+
|
|
335
|
+
# Process results
|
|
336
|
+
for name, result in zip(to_install, install_results):
|
|
337
|
+
if isinstance(result, Exception):
|
|
338
|
+
install_result = InstallResult(
|
|
339
|
+
package_name=name,
|
|
340
|
+
success=False,
|
|
341
|
+
status=InstallStatus.FAILED,
|
|
342
|
+
error=str(result)
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
install_result = result
|
|
346
|
+
|
|
347
|
+
results[name] = install_result
|
|
348
|
+
|
|
349
|
+
# Update cache for successful installs
|
|
350
|
+
if install_result.success:
|
|
351
|
+
self._install_cache.mark_installed(
|
|
352
|
+
name,
|
|
353
|
+
install_result.version
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Call callback if provided
|
|
357
|
+
if callback:
|
|
358
|
+
callback(name, install_result)
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"Error in install_many: {e}")
|
|
362
|
+
# Mark remaining as failed
|
|
363
|
+
for name in to_install:
|
|
364
|
+
if name not in results:
|
|
365
|
+
results[name] = InstallResult(
|
|
366
|
+
package_name=name,
|
|
367
|
+
success=False,
|
|
368
|
+
status=InstallStatus.FAILED,
|
|
369
|
+
error=str(e)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return results
|
|
373
|
+
|
|
374
|
+
def install_one(
|
|
375
|
+
self,
|
|
376
|
+
package_name: str,
|
|
377
|
+
module_name: Optional[str] = None
|
|
378
|
+
) -> InstallResult:
|
|
379
|
+
"""
|
|
380
|
+
Install a single package (synchronous interface).
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
package_name: Package name to install
|
|
384
|
+
module_name: Optional module name (for context)
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
InstallResult with success status
|
|
388
|
+
"""
|
|
389
|
+
results = self.install_many(package_name)
|
|
390
|
+
return results.get(package_name, InstallResult(
|
|
391
|
+
package_name=package_name,
|
|
392
|
+
success=False,
|
|
393
|
+
status=InstallStatus.FAILED,
|
|
394
|
+
error="Installation not executed"
|
|
395
|
+
))
|
|
396
|
+
|
|
397
|
+
def set_mode(self, mode: LazyInstallMode) -> None:
|
|
398
|
+
"""Set installation mode."""
|
|
399
|
+
with self._lock:
|
|
400
|
+
self._mode = mode
|
|
401
|
+
|
|
402
|
+
def get_mode(self) -> LazyInstallMode:
|
|
403
|
+
"""Get current installation mode."""
|
|
404
|
+
return self._mode
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
__all__ = ['InstallerEngine']
|
|
408
|
+
|