exonware-xwlazy 0.1.0.22__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 (90) hide show
  1. exonware/__init__.py +86 -16
  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.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/METADATA +6 -6
  6. exonware_xwlazy-1.0.1.2.dist-info/RECORD +8 -0
  7. exonware/xwlazy/__init__.py +0 -367
  8. exonware/xwlazy/common/__init__.py +0 -47
  9. exonware/xwlazy/common/base.py +0 -56
  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 -232
  14. exonware/xwlazy/common/services/install_async_utils.py +0 -165
  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/config.py +0 -193
  27. exonware/xwlazy/contracts.py +0 -1396
  28. exonware/xwlazy/defs.py +0 -378
  29. exonware/xwlazy/errors.py +0 -276
  30. exonware/xwlazy/facade.py +0 -991
  31. exonware/xwlazy/module/__init__.py +0 -18
  32. exonware/xwlazy/module/base.py +0 -565
  33. exonware/xwlazy/module/data.py +0 -17
  34. exonware/xwlazy/module/facade.py +0 -246
  35. exonware/xwlazy/module/importer_engine.py +0 -2117
  36. exonware/xwlazy/module/strategies/__init__.py +0 -22
  37. exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
  38. exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
  39. exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
  40. exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
  41. exonware/xwlazy/package/__init__.py +0 -18
  42. exonware/xwlazy/package/base.py +0 -798
  43. exonware/xwlazy/package/conf.py +0 -324
  44. exonware/xwlazy/package/data.py +0 -17
  45. exonware/xwlazy/package/facade.py +0 -480
  46. exonware/xwlazy/package/services/__init__.py +0 -84
  47. exonware/xwlazy/package/services/async_install_handle.py +0 -87
  48. exonware/xwlazy/package/services/config_manager.py +0 -245
  49. exonware/xwlazy/package/services/discovery.py +0 -370
  50. exonware/xwlazy/package/services/host_packages.py +0 -145
  51. exonware/xwlazy/package/services/install_async.py +0 -277
  52. exonware/xwlazy/package/services/install_cache.py +0 -145
  53. exonware/xwlazy/package/services/install_interactive.py +0 -59
  54. exonware/xwlazy/package/services/install_policy.py +0 -156
  55. exonware/xwlazy/package/services/install_registry.py +0 -54
  56. exonware/xwlazy/package/services/install_result.py +0 -17
  57. exonware/xwlazy/package/services/install_sbom.py +0 -153
  58. exonware/xwlazy/package/services/install_utils.py +0 -79
  59. exonware/xwlazy/package/services/installer_engine.py +0 -406
  60. exonware/xwlazy/package/services/lazy_installer.py +0 -718
  61. exonware/xwlazy/package/services/manifest.py +0 -496
  62. exonware/xwlazy/package/services/strategy_registry.py +0 -186
  63. exonware/xwlazy/package/strategies/__init__.py +0 -57
  64. exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
  65. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
  66. exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
  67. exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
  68. exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
  69. exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
  70. exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
  71. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
  72. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
  73. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
  74. exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
  75. exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
  76. exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
  77. exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
  78. exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
  79. exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
  80. exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
  81. exonware/xwlazy/runtime/__init__.py +0 -18
  82. exonware/xwlazy/runtime/adaptive_learner.py +0 -129
  83. exonware/xwlazy/runtime/base.py +0 -274
  84. exonware/xwlazy/runtime/facade.py +0 -94
  85. exonware/xwlazy/runtime/intelligent_selector.py +0 -170
  86. exonware/xwlazy/runtime/metrics.py +0 -60
  87. exonware/xwlazy/runtime/performance.py +0 -37
  88. exonware_xwlazy-0.1.0.22.dist-info/RECORD +0 -87
  89. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/WHEEL +0 -0
  90. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,145 +0,0 @@
1
- """Lightweight helpers for registering host packages with xwlazy."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import sys
7
- from typing import Iterable, Sequence, Tuple
8
-
9
- # Lazy imports to avoid circular dependency
10
- def _get_config_package_lazy_install_enabled():
11
- """Get config_package_lazy_install_enabled (lazy import to avoid circular dependency)."""
12
- from ...facade import config_package_lazy_install_enabled
13
- return config_package_lazy_install_enabled
14
-
15
- def _get_install_import_hook():
16
- """Get install_import_hook (lazy import to avoid circular dependency)."""
17
- from ...facade import install_import_hook
18
- return install_import_hook
19
-
20
- def _get_is_lazy_install_enabled():
21
- """Get is_lazy_install_enabled (lazy import to avoid circular dependency)."""
22
- from ...facade import is_lazy_install_enabled
23
- return is_lazy_install_enabled
24
-
25
- def _get_register_lazy_module_methods():
26
- """Get register_lazy_module_methods (lazy import to avoid circular dependency)."""
27
- from ...facade import register_lazy_module_methods
28
- return register_lazy_module_methods
29
-
30
- def _get_register_lazy_module_prefix():
31
- """Get register_lazy_module_prefix (lazy import to avoid circular dependency)."""
32
- from ...facade import register_lazy_module_prefix
33
- return register_lazy_module_prefix
34
-
35
- # Note: LazyMetaPathFinder is implemented in module/meta_path_finder.py
36
- # For now, create a placeholder
37
- class LazyMetaPathFinder:
38
- """Placeholder for LazyMetaPathFinder - to be implemented in hooks domain."""
39
- def __init__(self, package_name: str):
40
- self.package_name = package_name
41
-
42
- def _enhance_classes_with_class_methods(self, module):
43
- """Placeholder method."""
44
- pass
45
-
46
- _TRUTHY = {"1", "true", "yes", "on"}
47
- _REGISTERED: dict[str, dict[str, Tuple[str, ...]]] = {}
48
-
49
- def _normalized(items: Iterable[str]) -> Tuple[str, ...]:
50
- seen = []
51
- for item in items:
52
- if item not in seen:
53
- seen.append(item)
54
- return tuple(seen)
55
-
56
- def register_host_package(
57
- package_name: str,
58
- module_prefixes: Iterable[str] = (),
59
- method_prefixes: Iterable[str] = (),
60
- method_names: Sequence[str] = ("encode", "decode"),
61
- auto_config: bool = True,
62
- env_var: str | None = None,
63
- ) -> None:
64
- """
65
- Register a host package (e.g., xwsystem) with xwlazy.
66
-
67
- Args:
68
- package_name: Host package name.
69
- module_prefixes: Prefixes that should be lazily wrapped.
70
- method_prefixes: Prefixes whose classes expose class-level helpers.
71
- method_names: Methods to expose at class level (default encode/decode).
72
- auto_config: If True, record lazy config but do not install hook yet.
73
- env_var: Optional environment variable to force enable (defaults to
74
- ``{PACKAGE}_LAZY_INSTALL``).
75
- """
76
- package_name = package_name.lower()
77
-
78
- module_prefixes = _normalized(module_prefixes)
79
- method_prefixes = _normalized(method_prefixes)
80
- _REGISTERED[package_name] = {
81
- "module_prefixes": module_prefixes,
82
- "method_prefixes": method_prefixes,
83
- }
84
-
85
- for prefix in module_prefixes:
86
- _get_register_lazy_module_prefix()(prefix)
87
-
88
- for prefix in method_prefixes:
89
- _get_register_lazy_module_methods()(prefix, tuple(method_names))
90
-
91
- if auto_config:
92
- # Detect if lazy should be enabled (checks keyword, marker package, etc.)
93
- _get_config_package_lazy_install_enabled()(package_name, enabled=None, install_hook=False)
94
-
95
- # If detection found that lazy should be enabled, install the hook automatically
96
- if _get_is_lazy_install_enabled()(package_name):
97
- try:
98
- _get_install_import_hook()(package_name)
99
- except Exception:
100
- # Best-effort: package import must continue even if hook installation fails
101
- pass
102
-
103
- _apply_wrappers_for_loaded_modules(package_name, module_prefixes, method_prefixes)
104
-
105
- env_key = env_var or f"{package_name.upper()}_LAZY_INSTALL"
106
- flag = os.environ.get(env_key, "")
107
- if flag.strip().lower() in _TRUTHY:
108
- _get_config_package_lazy_install_enabled()(package_name, enabled=True)
109
- try:
110
- _get_install_import_hook()(package_name)
111
- except Exception:
112
- # Best-effort: package import must continue even if hook installation fails
113
- pass
114
-
115
- def refresh_host_package(package_name: str) -> None:
116
- """Re-apply wrappers for a registered package."""
117
- data = _REGISTERED.get(package_name.lower())
118
- if not data:
119
- return
120
- _apply_wrappers_for_loaded_modules(
121
- package_name,
122
- data["module_prefixes"],
123
- data["method_prefixes"],
124
- )
125
-
126
- def _apply_wrappers_for_loaded_modules(
127
- package_name: str,
128
- module_prefixes: Iterable[str],
129
- method_prefixes: Iterable[str],
130
- ) -> None:
131
- """Enhance already-imported modules so encode/decode helpers work immediately."""
132
- prefixes = _normalized((*module_prefixes, *method_prefixes))
133
- if not prefixes:
134
- return
135
-
136
- finder = LazyMetaPathFinder(package_name)
137
- for module_name, module in list(sys.modules.items()):
138
- if not isinstance(module_name, str) or module is None:
139
- continue
140
- if any(module_name.startswith(prefix) for prefix in prefixes):
141
- try:
142
- finder._enhance_classes_with_class_methods(module) # type: ignore[attr-defined]
143
- except Exception:
144
- continue
145
-
@@ -1,277 +0,0 @@
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
-
@@ -1,145 +0,0 @@
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
-
@@ -1,59 +0,0 @@
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
-