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.
Files changed (96) hide show
  1. exonware/__init__.py +22 -0
  2. exonware/xwlazy/__init__.py +0 -0
  3. exonware/xwlazy/common/__init__.py +47 -0
  4. exonware/xwlazy/common/base.py +58 -0
  5. exonware/xwlazy/common/cache.py +506 -0
  6. exonware/xwlazy/common/logger.py +268 -0
  7. exonware/xwlazy/common/services/__init__.py +72 -0
  8. exonware/xwlazy/common/services/dependency_mapper.py +234 -0
  9. exonware/xwlazy/common/services/install_async_utils.py +169 -0
  10. exonware/xwlazy/common/services/install_cache_utils.py +257 -0
  11. exonware/xwlazy/common/services/keyword_detection.py +292 -0
  12. exonware/xwlazy/common/services/spec_cache.py +173 -0
  13. exonware/xwlazy/common/strategies/__init__.py +28 -0
  14. exonware/xwlazy/common/strategies/caching_dict.py +45 -0
  15. exonware/xwlazy/common/strategies/caching_installation.py +89 -0
  16. exonware/xwlazy/common/strategies/caching_lfu.py +67 -0
  17. exonware/xwlazy/common/strategies/caching_lru.py +64 -0
  18. exonware/xwlazy/common/strategies/caching_multitier.py +60 -0
  19. exonware/xwlazy/common/strategies/caching_ttl.py +60 -0
  20. {xwlazy/lazy → exonware/xwlazy}/config.py +52 -20
  21. exonware/xwlazy/contracts.py +1410 -0
  22. exonware/xwlazy/defs.py +397 -0
  23. xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -8
  24. exonware/xwlazy/facade.py +1049 -0
  25. exonware/xwlazy/module/__init__.py +18 -0
  26. exonware/xwlazy/module/base.py +569 -0
  27. exonware/xwlazy/module/data.py +17 -0
  28. exonware/xwlazy/module/facade.py +247 -0
  29. exonware/xwlazy/module/importer_engine.py +2161 -0
  30. exonware/xwlazy/module/strategies/__init__.py +22 -0
  31. exonware/xwlazy/module/strategies/module_helper_lazy.py +94 -0
  32. exonware/xwlazy/module/strategies/module_helper_simple.py +66 -0
  33. exonware/xwlazy/module/strategies/module_manager_advanced.py +112 -0
  34. exonware/xwlazy/module/strategies/module_manager_simple.py +96 -0
  35. exonware/xwlazy/package/__init__.py +18 -0
  36. exonware/xwlazy/package/base.py +807 -0
  37. xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +62 -10
  38. exonware/xwlazy/package/data.py +17 -0
  39. exonware/xwlazy/package/facade.py +481 -0
  40. exonware/xwlazy/package/services/__init__.py +84 -0
  41. exonware/xwlazy/package/services/async_install_handle.py +89 -0
  42. exonware/xwlazy/package/services/config_manager.py +246 -0
  43. exonware/xwlazy/package/services/discovery.py +374 -0
  44. {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -16
  45. exonware/xwlazy/package/services/install_async.py +278 -0
  46. exonware/xwlazy/package/services/install_cache.py +146 -0
  47. exonware/xwlazy/package/services/install_interactive.py +60 -0
  48. exonware/xwlazy/package/services/install_policy.py +158 -0
  49. exonware/xwlazy/package/services/install_registry.py +56 -0
  50. exonware/xwlazy/package/services/install_result.py +17 -0
  51. exonware/xwlazy/package/services/install_sbom.py +154 -0
  52. exonware/xwlazy/package/services/install_utils.py +83 -0
  53. exonware/xwlazy/package/services/installer_engine.py +408 -0
  54. exonware/xwlazy/package/services/lazy_installer.py +720 -0
  55. {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +42 -25
  56. exonware/xwlazy/package/services/strategy_registry.py +188 -0
  57. exonware/xwlazy/package/strategies/__init__.py +57 -0
  58. exonware/xwlazy/package/strategies/package_discovery_file.py +130 -0
  59. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +85 -0
  60. exonware/xwlazy/package/strategies/package_discovery_manifest.py +102 -0
  61. exonware/xwlazy/package/strategies/package_execution_async.py +114 -0
  62. exonware/xwlazy/package/strategies/package_execution_cached.py +91 -0
  63. exonware/xwlazy/package/strategies/package_execution_pip.py +100 -0
  64. exonware/xwlazy/package/strategies/package_execution_wheel.py +107 -0
  65. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +101 -0
  66. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +106 -0
  67. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +101 -0
  68. exonware/xwlazy/package/strategies/package_policy_allow_list.py +58 -0
  69. exonware/xwlazy/package/strategies/package_policy_deny_list.py +58 -0
  70. exonware/xwlazy/package/strategies/package_policy_permissive.py +47 -0
  71. exonware/xwlazy/package/strategies/package_timing_clean.py +68 -0
  72. exonware/xwlazy/package/strategies/package_timing_full.py +67 -0
  73. exonware/xwlazy/package/strategies/package_timing_smart.py +69 -0
  74. exonware/xwlazy/package/strategies/package_timing_temporary.py +67 -0
  75. exonware/xwlazy/runtime/__init__.py +18 -0
  76. exonware/xwlazy/runtime/adaptive_learner.py +131 -0
  77. exonware/xwlazy/runtime/base.py +276 -0
  78. exonware/xwlazy/runtime/facade.py +95 -0
  79. exonware/xwlazy/runtime/intelligent_selector.py +173 -0
  80. exonware/xwlazy/runtime/metrics.py +64 -0
  81. exonware/xwlazy/runtime/performance.py +39 -0
  82. exonware/xwlazy/version.py +2 -2
  83. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/METADATA +86 -10
  84. exonware_xwlazy-0.1.0.19.dist-info/RECORD +87 -0
  85. exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
  86. xwlazy/__init__.py +0 -34
  87. xwlazy/lazy/__init__.py +0 -301
  88. xwlazy/lazy/bootstrap.py +0 -106
  89. xwlazy/lazy/lazy_base.py +0 -465
  90. xwlazy/lazy/lazy_contracts.py +0 -290
  91. xwlazy/lazy/lazy_core.py +0 -3727
  92. xwlazy/lazy/logging_utils.py +0 -194
  93. xwlazy/version.py +0 -77
  94. /xwlazy/lazy/lazy_state.py → /exonware/xwlazy/common/services/state_manager.py +0 -0
  95. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/WHEEL +0 -0
  96. {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
+