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