exonware-xwlazy 0.1.0.11__py3-none-any.whl → 0.1.0.20__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 +26 -0
  2. exonware/xwlazy/__init__.py +0 -0
  3. exonware/xwlazy/common/__init__.py +47 -0
  4. exonware/xwlazy/common/base.py +56 -0
  5. exonware/xwlazy/common/cache.py +504 -0
  6. exonware/xwlazy/common/logger.py +257 -0
  7. exonware/xwlazy/common/services/__init__.py +72 -0
  8. exonware/xwlazy/common/services/dependency_mapper.py +232 -0
  9. exonware/xwlazy/common/services/install_async_utils.py +165 -0
  10. exonware/xwlazy/common/services/install_cache_utils.py +245 -0
  11. exonware/xwlazy/common/services/keyword_detection.py +283 -0
  12. exonware/xwlazy/common/services/spec_cache.py +165 -0
  13. xwlazy/lazy/lazy_state.py → exonware/xwlazy/common/services/state_manager.py +0 -2
  14. exonware/xwlazy/common/strategies/__init__.py +28 -0
  15. exonware/xwlazy/common/strategies/caching_dict.py +44 -0
  16. exonware/xwlazy/common/strategies/caching_installation.py +88 -0
  17. exonware/xwlazy/common/strategies/caching_lfu.py +66 -0
  18. exonware/xwlazy/common/strategies/caching_lru.py +63 -0
  19. exonware/xwlazy/common/strategies/caching_multitier.py +59 -0
  20. exonware/xwlazy/common/strategies/caching_ttl.py +59 -0
  21. {xwlazy/lazy → exonware/xwlazy}/config.py +51 -21
  22. exonware/xwlazy/contracts.py +1396 -0
  23. exonware/xwlazy/defs.py +378 -0
  24. xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -16
  25. exonware/xwlazy/facade.py +991 -0
  26. exonware/xwlazy/module/__init__.py +18 -0
  27. exonware/xwlazy/module/base.py +565 -0
  28. exonware/xwlazy/module/data.py +17 -0
  29. exonware/xwlazy/module/facade.py +246 -0
  30. exonware/xwlazy/module/importer_engine.py +2117 -0
  31. exonware/xwlazy/module/strategies/__init__.py +22 -0
  32. exonware/xwlazy/module/strategies/module_helper_lazy.py +93 -0
  33. exonware/xwlazy/module/strategies/module_helper_simple.py +65 -0
  34. exonware/xwlazy/module/strategies/module_manager_advanced.py +111 -0
  35. exonware/xwlazy/module/strategies/module_manager_simple.py +95 -0
  36. exonware/xwlazy/package/__init__.py +18 -0
  37. exonware/xwlazy/package/base.py +798 -0
  38. xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +61 -16
  39. exonware/xwlazy/package/data.py +17 -0
  40. exonware/xwlazy/package/facade.py +480 -0
  41. exonware/xwlazy/package/services/__init__.py +84 -0
  42. exonware/xwlazy/package/services/async_install_handle.py +87 -0
  43. exonware/xwlazy/package/services/config_manager.py +245 -0
  44. exonware/xwlazy/package/services/discovery.py +370 -0
  45. {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -20
  46. exonware/xwlazy/package/services/install_async.py +277 -0
  47. exonware/xwlazy/package/services/install_cache.py +145 -0
  48. exonware/xwlazy/package/services/install_interactive.py +59 -0
  49. exonware/xwlazy/package/services/install_policy.py +156 -0
  50. exonware/xwlazy/package/services/install_registry.py +54 -0
  51. exonware/xwlazy/package/services/install_result.py +17 -0
  52. exonware/xwlazy/package/services/install_sbom.py +153 -0
  53. exonware/xwlazy/package/services/install_utils.py +79 -0
  54. exonware/xwlazy/package/services/installer_engine.py +406 -0
  55. exonware/xwlazy/package/services/lazy_installer.py +718 -0
  56. {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +40 -33
  57. exonware/xwlazy/package/services/strategy_registry.py +186 -0
  58. exonware/xwlazy/package/strategies/__init__.py +57 -0
  59. exonware/xwlazy/package/strategies/package_discovery_file.py +129 -0
  60. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +84 -0
  61. exonware/xwlazy/package/strategies/package_discovery_manifest.py +101 -0
  62. exonware/xwlazy/package/strategies/package_execution_async.py +113 -0
  63. exonware/xwlazy/package/strategies/package_execution_cached.py +90 -0
  64. exonware/xwlazy/package/strategies/package_execution_pip.py +99 -0
  65. exonware/xwlazy/package/strategies/package_execution_wheel.py +106 -0
  66. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +100 -0
  67. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +105 -0
  68. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +100 -0
  69. exonware/xwlazy/package/strategies/package_policy_allow_list.py +57 -0
  70. exonware/xwlazy/package/strategies/package_policy_deny_list.py +57 -0
  71. exonware/xwlazy/package/strategies/package_policy_permissive.py +46 -0
  72. exonware/xwlazy/package/strategies/package_timing_clean.py +67 -0
  73. exonware/xwlazy/package/strategies/package_timing_full.py +66 -0
  74. exonware/xwlazy/package/strategies/package_timing_smart.py +68 -0
  75. exonware/xwlazy/package/strategies/package_timing_temporary.py +66 -0
  76. exonware/xwlazy/runtime/__init__.py +18 -0
  77. exonware/xwlazy/runtime/adaptive_learner.py +129 -0
  78. exonware/xwlazy/runtime/base.py +274 -0
  79. exonware/xwlazy/runtime/facade.py +94 -0
  80. exonware/xwlazy/runtime/intelligent_selector.py +170 -0
  81. exonware/xwlazy/runtime/metrics.py +60 -0
  82. exonware/xwlazy/runtime/performance.py +37 -0
  83. exonware/xwlazy/version.py +2 -2
  84. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/METADATA +89 -11
  85. exonware_xwlazy-0.1.0.20.dist-info/RECORD +87 -0
  86. exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
  87. xwlazy/__init__.py +0 -34
  88. xwlazy/lazy/__init__.py +0 -301
  89. xwlazy/lazy/bootstrap.py +0 -106
  90. xwlazy/lazy/lazy_base.py +0 -465
  91. xwlazy/lazy/lazy_contracts.py +0 -290
  92. xwlazy/lazy/lazy_core.py +0 -3727
  93. xwlazy/lazy/logging_utils.py +0 -194
  94. xwlazy/version.py +0 -77
  95. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/WHEEL +0 -0
  96. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,277 @@
1
+ """
2
+ Async Installation Mixin
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+
8
+ Generation Date: 15-Nov-2025
9
+
10
+ Mixin for async installation operations.
11
+ """
12
+
13
+ import os
14
+ import asyncio
15
+ import threading
16
+ import importlib
17
+ from typing import Optional, Dict, Any
18
+
19
+ from .async_install_handle import AsyncInstallHandle
20
+ from .manifest import PackageManifest
21
+ from .config_manager import LazyInstallConfig
22
+ from .install_policy import LazyInstallPolicy
23
+ from ...defs import LazyInstallMode
24
+
25
+ # Lazy imports
26
+ def _get_logger():
27
+ """Get logger (lazy import to avoid circular dependency)."""
28
+ from ...common.logger import get_logger
29
+ return get_logger("xwlazy.lazy_installer")
30
+
31
+ def _get_log_event():
32
+ """Get log_event function (lazy import to avoid circular dependency)."""
33
+ from ...common.logger import log_event
34
+ return log_event
35
+
36
+ logger = None
37
+ _log = None
38
+
39
+ # Environment variables
40
+ _ENV_ASYNC_INSTALL = os.environ.get("XWLAZY_ASYNC_INSTALL", "").strip().lower() in {"1", "true", "yes", "on"}
41
+ _ENV_ASYNC_WORKERS = int(os.environ.get("XWLAZY_ASYNC_WORKERS", "0") or 0)
42
+
43
+ def _ensure_logging_initialized():
44
+ """Ensure logging utilities are initialized."""
45
+ global logger, _log
46
+ if logger is None:
47
+ logger = _get_logger()
48
+ if _log is None:
49
+ _log = _get_log_event()
50
+
51
+ class AsyncInstallMixin:
52
+ """Mixin for async installation operations."""
53
+
54
+ def _ensure_async_loop(self) -> asyncio.AbstractEventLoop:
55
+ """Ensure async event loop is running in background thread."""
56
+ if self._async_loop is not None and self._async_loop.is_running(): # type: ignore[attr-defined]
57
+ return self._async_loop # type: ignore[attr-defined]
58
+
59
+ with self._lock: # type: ignore[attr-defined]
60
+ if self._async_loop is None or not self._async_loop.is_running(): # type: ignore[attr-defined]
61
+ loop_ready = threading.Event()
62
+ loop_ref = [None]
63
+
64
+ def _run_loop():
65
+ loop = asyncio.new_event_loop()
66
+ asyncio.set_event_loop(loop)
67
+ loop_ref[0] = loop
68
+ self._async_loop = loop # type: ignore[attr-defined]
69
+ loop_ready.set()
70
+ loop.run_forever()
71
+
72
+ self._loop_thread = threading.Thread( # type: ignore[attr-defined]
73
+ target=_run_loop,
74
+ daemon=True,
75
+ name=f"xwlazy-{self._package_name}-async" # type: ignore[attr-defined]
76
+ )
77
+ self._loop_thread.start() # type: ignore[attr-defined]
78
+
79
+ if not loop_ready.wait(timeout=5.0):
80
+ raise RuntimeError(f"Failed to start async loop for {self._package_name}") # type: ignore[attr-defined]
81
+
82
+ return self._async_loop # type: ignore[attr-defined]
83
+
84
+ def apply_manifest(self, manifest: Optional[PackageManifest]) -> None:
85
+ """Apply manifest-driven configuration such as async installs."""
86
+ env_override = _ENV_ASYNC_INSTALL
87
+ desired_async = bool(env_override or (manifest and manifest.async_installs))
88
+ desired_workers = _ENV_ASYNC_WORKERS or (manifest.async_workers if manifest else 1)
89
+ desired_workers = max(1, desired_workers)
90
+
91
+ with self._lock: # type: ignore[attr-defined]
92
+ self._async_workers = desired_workers # type: ignore[attr-defined]
93
+
94
+ if desired_async:
95
+ self._ensure_async_loop()
96
+ else:
97
+ if self._async_loop is not None: # type: ignore[attr-defined]
98
+ for task in list(self._async_tasks.values()): # type: ignore[attr-defined]
99
+ if not task.done():
100
+ task.cancel()
101
+ self._async_tasks.clear() # type: ignore[attr-defined]
102
+
103
+ self._async_enabled = desired_async # type: ignore[attr-defined]
104
+
105
+ def is_async_enabled(self) -> bool:
106
+ """Return True if async installers are enabled for this package."""
107
+ return self._async_enabled # type: ignore[attr-defined]
108
+
109
+ def ensure_async_install(self, module_name: str) -> Optional[AsyncInstallHandle]:
110
+ """Schedule (or reuse) an async install job for module_name if async is enabled."""
111
+ if not self._async_enabled: # type: ignore[attr-defined]
112
+ return None
113
+ return self.schedule_async_install(module_name)
114
+
115
+ async def _get_package_size_mb(self, package_name: str) -> Optional[float]:
116
+ """Get package size in MB by checking pip show or download size."""
117
+ from ...common.services.install_async_utils import get_package_size_mb
118
+ return await get_package_size_mb(package_name)
119
+
120
+ async def _async_install_package(self, package_name: str, module_name: str) -> bool:
121
+ """Async version of install_package using asyncio subprocess."""
122
+ _ensure_logging_initialized()
123
+ # SIZE_AWARE mode: Check package size before installing
124
+ if self._mode == LazyInstallMode.SIZE_AWARE: # type: ignore[attr-defined]
125
+ mode_config = LazyInstallConfig.get_mode_config(self._package_name) # type: ignore[attr-defined]
126
+ threshold_mb = mode_config.large_package_threshold_mb if mode_config else 50.0
127
+
128
+ size_mb = await self._get_package_size_mb(package_name)
129
+ if size_mb is not None and size_mb >= threshold_mb:
130
+ logger.warning(
131
+ f"Package '{package_name}' is {size_mb:.1f}MB (>= {threshold_mb}MB threshold), "
132
+ f"skipping installation in SIZE_AWARE mode"
133
+ )
134
+ print(
135
+ f"[SIZE_AWARE] Skipping large package '{package_name}' "
136
+ f"({size_mb:.1f}MB >= {threshold_mb}MB)"
137
+ )
138
+ self._failed_packages.add(package_name) # type: ignore[attr-defined]
139
+ return False
140
+
141
+ # Check cache first
142
+ if self._install_from_cached_tree(package_name): # type: ignore[attr-defined]
143
+ self._finalize_install_success(package_name, "cache-tree") # type: ignore[attr-defined]
144
+ return True
145
+
146
+ # Use asyncio subprocess for pip install
147
+ try:
148
+ policy_args = LazyInstallPolicy.get_pip_args(self._package_name) or [] # type: ignore[attr-defined]
149
+ from ...common.services.install_async_utils import async_install_package
150
+ success, error_msg = await async_install_package(package_name, policy_args)
151
+
152
+ if success:
153
+ self._finalize_install_success(package_name, "pip-async") # type: ignore[attr-defined]
154
+
155
+ # For CLEAN mode, schedule async uninstall after completion
156
+ if self._mode == LazyInstallMode.CLEAN: # type: ignore[attr-defined]
157
+ asyncio.create_task(self._schedule_clean_uninstall(package_name))
158
+
159
+ # For TEMPORARY mode, uninstall immediately after installation
160
+ if self._mode == LazyInstallMode.TEMPORARY: # type: ignore[attr-defined]
161
+ asyncio.create_task(self.uninstall_package_async(package_name, quiet=True))
162
+
163
+ return True
164
+ else:
165
+ self._failed_packages.add(package_name) # type: ignore[attr-defined]
166
+ return False
167
+ except Exception as e:
168
+ logger.error(f"Error in async install of {package_name}: {e}")
169
+ self._failed_packages.add(package_name) # type: ignore[attr-defined]
170
+ return False
171
+
172
+ async def _schedule_clean_uninstall(self, package_name: str) -> None:
173
+ """Schedule uninstall for CLEAN mode after a delay."""
174
+ await asyncio.sleep(1.0)
175
+ await self.uninstall_package_async(package_name, quiet=True)
176
+
177
+ async def uninstall_package_async(self, package_name: str, quiet: bool = True) -> bool:
178
+ """Uninstall a package asynchronously in quiet mode."""
179
+ with self._lock: # type: ignore[attr-defined]
180
+ if package_name not in self._installed_packages: # type: ignore[attr-defined]
181
+ return True
182
+
183
+ from ...common.services.install_async_utils import async_uninstall_package
184
+ success = await async_uninstall_package(package_name, quiet)
185
+
186
+ if success:
187
+ with self._lock: # type: ignore[attr-defined]
188
+ self._installed_packages.discard(package_name) # type: ignore[attr-defined]
189
+
190
+ return success
191
+
192
+ def schedule_async_install(self, module_name: str) -> Optional[AsyncInstallHandle]:
193
+ """Schedule installation of a dependency in the background using asyncio."""
194
+ _ensure_logging_initialized()
195
+ if not self._async_enabled: # type: ignore[attr-defined]
196
+ return None
197
+
198
+ package_name = self._dependency_mapper.get_package_name(module_name) or module_name # type: ignore[attr-defined]
199
+ if not package_name:
200
+ return None
201
+
202
+ with self._lock: # type: ignore[attr-defined]
203
+ task = self._async_tasks.get(module_name) # type: ignore[attr-defined]
204
+ if task is None or task.done():
205
+ self._mark_module_missing(module_name) # type: ignore[attr-defined]
206
+ loop = self._ensure_async_loop()
207
+
208
+ async def _install_and_cleanup():
209
+ try:
210
+ result = await self._async_install_package(package_name, module_name)
211
+ if result:
212
+ self._clear_module_missing(module_name) # type: ignore[attr-defined]
213
+ try:
214
+ imported_module = importlib.import_module(module_name)
215
+ if self._mode == LazyInstallMode.TEMPORARY: # type: ignore[attr-defined]
216
+ asyncio.create_task(self.uninstall_package_async(package_name, quiet=True))
217
+ except Exception:
218
+ pass
219
+ return result
220
+ finally:
221
+ with self._lock: # type: ignore[attr-defined]
222
+ self._async_tasks.pop(module_name, None) # type: ignore[attr-defined]
223
+
224
+ task = asyncio.run_coroutine_threadsafe(_install_and_cleanup(), loop)
225
+ self._async_tasks[module_name] = task # type: ignore[attr-defined]
226
+
227
+ return AsyncInstallHandle(task, module_name, package_name, self._package_name) # type: ignore[attr-defined]
228
+
229
+ async def install_all_dependencies(self) -> None:
230
+ """Install all dependencies from discovered requirements (FULL mode)."""
231
+ _ensure_logging_initialized()
232
+ if self._mode != LazyInstallMode.FULL: # type: ignore[attr-defined]
233
+ return
234
+
235
+ try:
236
+ # Lazy import to avoid circular dependency
237
+ from .discovery import get_lazy_discovery
238
+ discovery = get_lazy_discovery()
239
+ if discovery:
240
+ all_deps = discovery.discover_all_dependencies()
241
+ if not all_deps:
242
+ return
243
+
244
+ packages_to_install = [
245
+ (import_name, package_name)
246
+ for import_name, package_name in all_deps.items()
247
+ if package_name not in self._installed_packages # type: ignore[attr-defined]
248
+ ]
249
+
250
+ if not packages_to_install:
251
+ _log("install", f"All dependencies already installed for {self._package_name}") # type: ignore[attr-defined]
252
+ return
253
+
254
+ _log(
255
+ "install",
256
+ f"Installing {len(packages_to_install)} dependencies for {self._package_name} (FULL mode)"
257
+ )
258
+
259
+ batch_size = min(self._async_workers * 2, 10) # type: ignore[attr-defined]
260
+ for i in range(0, len(packages_to_install), batch_size):
261
+ batch = packages_to_install[i:i + batch_size]
262
+ tasks = [
263
+ self._async_install_package(package_name, import_name)
264
+ for import_name, package_name in batch
265
+ ]
266
+ results = await asyncio.gather(*tasks, return_exceptions=True)
267
+
268
+ for (import_name, package_name), result in zip(batch, results):
269
+ if isinstance(result, Exception):
270
+ logger.error(f"Failed to install {package_name}: {result}")
271
+ elif result:
272
+ _log("install", f"✓ Installed {package_name}")
273
+
274
+ _log("install", f"Completed installing all dependencies for {self._package_name}") # type: ignore[attr-defined]
275
+ except Exception as e:
276
+ logger.warning(f"Failed to install all dependencies for {self._package_name}: {e}") # type: ignore[attr-defined]
277
+
@@ -0,0 +1,145 @@
1
+ """
2
+ Installation Cache Mixin
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+
8
+ Generation Date: 15-Nov-2025
9
+
10
+ Mixin for cache management (wheels, install trees, known missing modules).
11
+ Uses shared utilities from common/services/install_cache_utils.
12
+ """
13
+
14
+ import os
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Optional, List
18
+ from collections import OrderedDict
19
+
20
+ # Import shared utilities
21
+ from ...common.services.install_cache_utils import (
22
+ get_cache_dir,
23
+ get_wheel_path,
24
+ get_install_tree_dir,
25
+ get_site_packages_dir,
26
+ pip_install_from_path,
27
+ ensure_cached_wheel,
28
+ install_from_cached_tree as _install_from_cached_tree_util,
29
+ materialize_cached_tree as _materialize_cached_tree_util,
30
+ has_cached_install_tree as _has_cached_install_tree_util,
31
+ install_from_cached_wheel as _install_from_cached_wheel_util,
32
+ )
33
+
34
+ # Lazy imports
35
+ def _get_spec_cache_clear():
36
+ """Get spec_cache_clear function (lazy import to avoid circular dependency)."""
37
+ from ...common.services.spec_cache import _spec_cache_clear
38
+ return _spec_cache_clear
39
+
40
+ _spec_cache_clear = None
41
+
42
+ # Environment variables
43
+ _KNOWN_MISSING_CACHE_LIMIT = int(os.environ.get("XWLAZY_MISSING_CACHE_MAX", "128") or 128)
44
+ _KNOWN_MISSING_CACHE_TTL = float(os.environ.get("XWLAZY_MISSING_CACHE_TTL", "120") or 120.0)
45
+
46
+ def _ensure_spec_cache_initialized():
47
+ """Ensure spec cache utilities are initialized."""
48
+ global _spec_cache_clear
49
+ if _spec_cache_clear is None:
50
+ _spec_cache_clear = _get_spec_cache_clear()
51
+
52
+ class InstallCacheMixin:
53
+ """Mixin for cache management (wheels, install trees, known missing modules)."""
54
+
55
+ def _prune_known_missing(self) -> None:
56
+ """Remove stale entries from the known-missing cache."""
57
+ if not self._known_missing: # type: ignore[attr-defined]
58
+ return
59
+ now = time.monotonic()
60
+ with self._lock: # type: ignore[attr-defined]
61
+ while self._known_missing: # type: ignore[attr-defined]
62
+ _, ts = next(iter(self._known_missing.items())) # type: ignore[attr-defined]
63
+ if now - ts <= _KNOWN_MISSING_CACHE_TTL:
64
+ break
65
+ self._known_missing.popitem(last=False) # type: ignore[attr-defined]
66
+
67
+ def _mark_module_missing(self, module_name: str) -> None:
68
+ """Remember modules that failed to import recently."""
69
+ _ensure_spec_cache_initialized()
70
+ with self._lock: # type: ignore[attr-defined]
71
+ self._prune_known_missing()
72
+ _spec_cache_clear(module_name)
73
+ self._known_missing[module_name] = time.monotonic() # type: ignore[attr-defined]
74
+ while len(self._known_missing) > _KNOWN_MISSING_CACHE_LIMIT: # type: ignore[attr-defined]
75
+ self._known_missing.popitem(last=False) # type: ignore[attr-defined]
76
+
77
+ def _clear_module_missing(self, module_name: str) -> None:
78
+ """Remove a module from the known-missing cache."""
79
+ with self._lock: # type: ignore[attr-defined]
80
+ self._known_missing.pop(module_name, None) # type: ignore[attr-defined]
81
+
82
+ def is_module_known_missing(self, module_name: str) -> bool:
83
+ """Return True if module recently failed to import."""
84
+ self._prune_known_missing()
85
+ with self._lock: # type: ignore[attr-defined]
86
+ return module_name in self._known_missing # type: ignore[attr-defined]
87
+
88
+ def _get_async_cache_dir(self) -> Path:
89
+ """Get the async cache directory."""
90
+ return get_cache_dir(self._async_cache_dir) # type: ignore[attr-defined]
91
+
92
+ def _cached_wheel_name(self, package_name: str) -> Path:
93
+ """Get the cached wheel file path for a package."""
94
+ return get_wheel_path(package_name, self._async_cache_dir) # type: ignore[attr-defined]
95
+
96
+ def _install_from_cached_wheel(self, package_name: str, policy_args: Optional[List[str]] = None) -> bool:
97
+ """Install from a cached wheel file."""
98
+ return _install_from_cached_wheel_util(
99
+ package_name,
100
+ policy_args,
101
+ self._async_cache_dir # type: ignore[attr-defined]
102
+ )
103
+
104
+ def _pip_install_from_path(self, wheel_path: Path, policy_args: Optional[List[str]] = None) -> bool:
105
+ """Install a wheel file using pip."""
106
+ return pip_install_from_path(wheel_path, policy_args)
107
+
108
+ def _ensure_cached_wheel(self, package_name: str, policy_args: Optional[List[str]] = None) -> Optional[Path]:
109
+ """Ensure a wheel is cached, downloading it if necessary."""
110
+ return ensure_cached_wheel(
111
+ package_name,
112
+ policy_args,
113
+ self._async_cache_dir # type: ignore[attr-defined]
114
+ )
115
+
116
+ def _cached_install_dir(self, package_name: str) -> Path:
117
+ """Get the cached install directory for a package."""
118
+ return get_install_tree_dir(package_name, self._async_cache_dir) # type: ignore[attr-defined]
119
+
120
+ def _has_cached_install_tree(self, package_name: str) -> bool:
121
+ """Check if a cached install tree exists."""
122
+ return _has_cached_install_tree_util(
123
+ package_name,
124
+ self._async_cache_dir # type: ignore[attr-defined]
125
+ )
126
+
127
+ def _site_packages_dir(self) -> Path:
128
+ """Get the site-packages directory."""
129
+ return get_site_packages_dir()
130
+
131
+ def _install_from_cached_tree(self, package_name: str) -> bool:
132
+ """Install from a cached install tree."""
133
+ return _install_from_cached_tree_util(
134
+ package_name,
135
+ self._async_cache_dir # type: ignore[attr-defined]
136
+ )
137
+
138
+ def _materialize_cached_tree(self, package_name: str, wheel_path: Path) -> None:
139
+ """Materialize a cached install tree from a wheel file."""
140
+ _materialize_cached_tree_util(
141
+ package_name,
142
+ wheel_path,
143
+ self._async_cache_dir # type: ignore[attr-defined]
144
+ )
145
+
@@ -0,0 +1,59 @@
1
+ """
2
+ Interactive Installation Mixin
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+
8
+ Generation Date: 15-Nov-2025
9
+
10
+ Mixin for interactive user prompts during installation.
11
+ """
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from .lazy_installer import LazyInstaller
17
+
18
+ class InteractiveInstallMixin:
19
+ """Mixin for interactive user prompts during installation."""
20
+
21
+ def _ask_user_permission(self, package_name: str, module_name: str) -> bool:
22
+ """Ask user for permission to install a package."""
23
+ if self._auto_approve_all: # type: ignore[attr-defined]
24
+ return True
25
+
26
+ print(f"\n{'='*60}")
27
+ print(f"Lazy Installation Active - {self._package_name}") # type: ignore[attr-defined]
28
+ print(f"{'='*60}")
29
+ print(f"Package: {package_name}")
30
+ print(f"Module: {module_name}")
31
+ print(f"{'='*60}")
32
+ print(f"\nThe module '{module_name}' is not installed.")
33
+ print(f"Would you like to install '{package_name}'?")
34
+ print(f"\nOptions:")
35
+ print(f" [Y] Yes - Install this package")
36
+ print(f" [N] No - Skip this package")
37
+ print(f" [A] All - Install this and all future packages without asking")
38
+ print(f" [Q] Quit - Cancel and raise ImportError")
39
+ print(f"{'='*60}")
40
+
41
+ while True:
42
+ try:
43
+ choice = input("Your choice [Y/N/A/Q]: ").strip().upper()
44
+
45
+ if choice in ('Y', 'YES', ''):
46
+ return True
47
+ elif choice in ('N', 'NO'):
48
+ return False
49
+ elif choice in ('A', 'ALL'):
50
+ self._auto_approve_all = True # type: ignore[attr-defined]
51
+ return True
52
+ elif choice in ('Q', 'QUIT'):
53
+ raise KeyboardInterrupt("User cancelled installation")
54
+ else:
55
+ print(f"Invalid choice '{choice}'. Please enter Y, N, A, or Q.")
56
+ except (EOFError, KeyboardInterrupt):
57
+ print("\n❌ Installation cancelled by user")
58
+ return False
59
+
@@ -0,0 +1,156 @@
1
+ """
2
+ Install Policy
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+
8
+ Generation Date: 15-Nov-2025
9
+
10
+ Security and policy configuration for lazy installation.
11
+ """
12
+
13
+ import threading
14
+ from typing import Dict, List, Optional, Set, Tuple
15
+
16
+ # Lazy import to avoid circular dependency
17
+ def _get_log_event():
18
+ """Get log_event function (lazy import to avoid circular dependency)."""
19
+ from ...common.logger import log_event
20
+ return log_event
21
+
22
+ _log = None # Will be initialized on first use
23
+
24
+ class LazyInstallPolicy:
25
+ """
26
+ Security and policy configuration for lazy installation.
27
+ Per-package allow/deny lists, index URLs, and security settings.
28
+ """
29
+ __slots__ = ()
30
+
31
+ _allow_lists: Dict[str, Set[str]] = {}
32
+ _deny_lists: Dict[str, Set[str]] = {}
33
+ _index_urls: Dict[str, str] = {}
34
+ _extra_index_urls: Dict[str, List[str]] = {}
35
+ _trusted_hosts: Dict[str, List[str]] = {}
36
+ _require_hashes: Dict[str, bool] = {}
37
+ _verify_ssl: Dict[str, bool] = {}
38
+ _lockfile_paths: Dict[str, str] = {}
39
+ _lock = threading.RLock()
40
+
41
+ @classmethod
42
+ def _ensure_logging(cls):
43
+ """Ensure logging is initialized."""
44
+ global _log
45
+ if _log is None:
46
+ _log = _get_log_event()
47
+
48
+ @classmethod
49
+ def set_allow_list(cls, package_name: str, allowed_packages: List[str]) -> None:
50
+ """Set allow list for a package (only these can be installed)."""
51
+ cls._ensure_logging()
52
+ with cls._lock:
53
+ cls._allow_lists[package_name] = set(allowed_packages)
54
+ _log("config", f"Set allow list for {package_name}: {len(allowed_packages)} packages")
55
+
56
+ @classmethod
57
+ def set_deny_list(cls, package_name: str, denied_packages: List[str]) -> None:
58
+ """Set deny list for a package (these cannot be installed)."""
59
+ cls._ensure_logging()
60
+ with cls._lock:
61
+ cls._deny_lists[package_name] = set(denied_packages)
62
+ _log("config", f"Set deny list for {package_name}: {len(denied_packages)} packages")
63
+
64
+ @classmethod
65
+ def add_to_allow_list(cls, package_name: str, allowed_package: str) -> None:
66
+ """Add single package to allow list."""
67
+ with cls._lock:
68
+ if package_name not in cls._allow_lists:
69
+ cls._allow_lists[package_name] = set()
70
+ cls._allow_lists[package_name].add(allowed_package)
71
+
72
+ @classmethod
73
+ def add_to_deny_list(cls, package_name: str, denied_package: str) -> None:
74
+ """Add single package to deny list."""
75
+ with cls._lock:
76
+ if package_name not in cls._deny_lists:
77
+ cls._deny_lists[package_name] = set()
78
+ cls._deny_lists[package_name].add(denied_package)
79
+
80
+ @classmethod
81
+ def is_package_allowed(cls, installer_package: str, target_package: str) -> Tuple[bool, str]:
82
+ """Check if target_package can be installed by installer_package."""
83
+ with cls._lock:
84
+ if installer_package in cls._deny_lists:
85
+ if target_package in cls._deny_lists[installer_package]:
86
+ return False, f"Package '{target_package}' is in deny list"
87
+
88
+ if installer_package in cls._allow_lists:
89
+ if target_package not in cls._allow_lists[installer_package]:
90
+ return False, f"Package '{target_package}' not in allow list"
91
+
92
+ return True, "OK"
93
+
94
+ @classmethod
95
+ def set_index_url(cls, package_name: str, index_url: str) -> None:
96
+ """Set PyPI index URL for a package."""
97
+ cls._ensure_logging()
98
+ with cls._lock:
99
+ cls._index_urls[package_name] = index_url
100
+ _log("config", f"Set index URL for {package_name}: {index_url}")
101
+
102
+ @classmethod
103
+ def set_extra_index_urls(cls, package_name: str, urls: List[str]) -> None:
104
+ """Set extra index URLs for a package."""
105
+ cls._ensure_logging()
106
+ with cls._lock:
107
+ cls._extra_index_urls[package_name] = urls
108
+ _log("config", f"Set {len(urls)} extra index URLs for {package_name}")
109
+
110
+ @classmethod
111
+ def add_trusted_host(cls, package_name: str, host: str) -> None:
112
+ """Add trusted host for a package."""
113
+ with cls._lock:
114
+ if package_name not in cls._trusted_hosts:
115
+ cls._trusted_hosts[package_name] = []
116
+ cls._trusted_hosts[package_name].append(host)
117
+
118
+ @classmethod
119
+ def get_pip_args(cls, package_name: str) -> List[str]:
120
+ """Get pip install arguments for a package based on policy."""
121
+ args = []
122
+
123
+ with cls._lock:
124
+ if package_name in cls._index_urls:
125
+ args.extend(['--index-url', cls._index_urls[package_name]])
126
+
127
+ if package_name in cls._extra_index_urls:
128
+ for url in cls._extra_index_urls[package_name]:
129
+ args.extend(['--extra-index-url', url])
130
+
131
+ if package_name in cls._trusted_hosts:
132
+ for host in cls._trusted_hosts[package_name]:
133
+ args.extend(['--trusted-host', host])
134
+
135
+ if cls._require_hashes.get(package_name, False):
136
+ args.append('--require-hashes')
137
+
138
+ if not cls._verify_ssl.get(package_name, True):
139
+ args.append('--no-verify-ssl')
140
+
141
+ return args
142
+
143
+ @classmethod
144
+ def set_lockfile_path(cls, package_name: str, path: str) -> None:
145
+ """Set lockfile path for a package."""
146
+ with cls._lock:
147
+ cls._lockfile_paths[package_name] = path
148
+
149
+ @classmethod
150
+ def get_lockfile_path(cls, package_name: str) -> Optional[str]:
151
+ """Get lockfile path for a package."""
152
+ with cls._lock:
153
+ return cls._lockfile_paths.get(package_name)
154
+
155
+ __all__ = ['LazyInstallPolicy']
156
+