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,180 +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
- import functools
8
- from typing import Iterable, Sequence
9
-
10
- # Lazy imports to avoid circular dependency
11
- def _get_config_package_lazy_install_enabled():
12
- """Get config_package_lazy_install_enabled (lazy import to avoid circular dependency)."""
13
- from ...facade import config_package_lazy_install_enabled
14
- return config_package_lazy_install_enabled
15
-
16
- def _get_install_import_hook():
17
- """Get install_import_hook (lazy import to avoid circular dependency)."""
18
- from ...facade import install_import_hook
19
- return install_import_hook
20
-
21
- def _get_is_lazy_install_enabled():
22
- """Get is_lazy_install_enabled (lazy import to avoid circular dependency)."""
23
- from ...facade import is_lazy_install_enabled
24
- return is_lazy_install_enabled
25
-
26
- def _get_register_lazy_module_methods():
27
- """Get register_lazy_module_methods (lazy import to avoid circular dependency)."""
28
- from ...facade import register_lazy_module_methods
29
- return register_lazy_module_methods
30
-
31
- def _get_register_lazy_module_prefix():
32
- """Get register_lazy_module_prefix (lazy import to avoid circular dependency)."""
33
- from ...facade import register_lazy_module_prefix
34
- return register_lazy_module_prefix
35
-
36
- # Note: LazyMetaPathFinder is implemented in module/meta_path_finder.py
37
- # For now, create a placeholder
38
- class LazyMetaPathFinder:
39
- """Placeholder for LazyMetaPathFinder - to be implemented in hooks domain."""
40
- def __init__(self, package_name: str):
41
- self.package_name = package_name
42
-
43
- def _enhance_classes_with_class_methods(self, module):
44
- """Enhance classes with lazy-aware static/class method behavior."""
45
-
46
- for name, cls in vars(module).items():
47
- if not isinstance(cls, type):
48
- continue
49
-
50
- # Skip if not a serializer (heuristic)
51
- if not name.endswith('Serializer'):
52
- continue
53
-
54
- # Methods to patch
55
- for method_name in ['encode', 'decode']:
56
- if not hasattr(cls, method_name):
57
- continue
58
-
59
- original_method = getattr(cls, method_name)
60
- if not callable(original_method):
61
- continue
62
-
63
- # Check if already patched to avoid recursion/duplication
64
- if getattr(original_method, '_is_lazy_wrapper', False):
65
- continue
66
-
67
- @functools.wraps(original_method)
68
- def wrapper(first_arg, *args, **kwargs):
69
- # Check if first_arg is 'self' (instance of cls)
70
- if isinstance(first_arg, cls):
71
- return original_method(first_arg, *args, **kwargs)
72
-
73
- # Called as class method or static usage: Class.encode(data)
74
- # first_arg is actually the data
75
- # Instantiate to trigger lazy loading (__init__)
76
- instance = cls()
77
- return original_method(instance, first_arg, *args, **kwargs)
78
-
79
- wrapper._is_lazy_wrapper = True
80
- setattr(cls, method_name, wrapper)
81
-
82
- _TRUTHY = {"1", "true", "yes", "on"}
83
- _REGISTERED: dict[str, dict[str, tuple[str, ...]]] = {}
84
-
85
- def _normalized(items: Iterable[str]) -> tuple[str, ...]:
86
- seen = []
87
- for item in items:
88
- if item not in seen:
89
- seen.append(item)
90
- return tuple(seen)
91
-
92
- def register_host_package(
93
- package_name: str,
94
- module_prefixes: Iterable[str] = (),
95
- method_prefixes: Iterable[str] = (),
96
- method_names: Sequence[str] = ("encode", "decode"),
97
- auto_config: bool = True,
98
- env_var: str | None = None,
99
- ) -> None:
100
- """
101
- Register a host package (e.g., xwsystem) with xwlazy.
102
-
103
- Args:
104
- package_name: Host package name.
105
- module_prefixes: Prefixes that should be lazily wrapped.
106
- method_prefixes: Prefixes whose classes expose class-level helpers.
107
- method_names: Methods to expose at class level (default encode/decode).
108
- auto_config: If True, record lazy config but do not install hook yet.
109
- env_var: Optional environment variable to force enable (defaults to
110
- ``{PACKAGE}_LAZY_INSTALL``).
111
- """
112
- package_name = package_name.lower()
113
-
114
- module_prefixes = _normalized(module_prefixes)
115
- method_prefixes = _normalized(method_prefixes)
116
- _REGISTERED[package_name] = {
117
- "module_prefixes": module_prefixes,
118
- "method_prefixes": method_prefixes,
119
- }
120
-
121
- for prefix in module_prefixes:
122
- _get_register_lazy_module_prefix()(prefix)
123
-
124
- for prefix in method_prefixes:
125
- _get_register_lazy_module_methods()(prefix, tuple(method_names))
126
-
127
- if auto_config:
128
- # Detect if lazy should be enabled (checks keyword, marker package, etc.)
129
- _get_config_package_lazy_install_enabled()(package_name, enabled=None, install_hook=False)
130
-
131
- # If detection found that lazy should be enabled, install the hook automatically
132
- if _get_is_lazy_install_enabled()(package_name):
133
- try:
134
- _get_install_import_hook()(package_name)
135
- except Exception:
136
- # Best-effort: package import must continue even if hook installation fails
137
- pass
138
-
139
- _apply_wrappers_for_loaded_modules(package_name, module_prefixes, method_prefixes)
140
-
141
- env_key = env_var or f"{package_name.upper()}_LAZY_INSTALL"
142
- flag = os.environ.get(env_key, "")
143
- if flag.strip().lower() in _TRUTHY:
144
- _get_config_package_lazy_install_enabled()(package_name, enabled=True)
145
- try:
146
- _get_install_import_hook()(package_name)
147
- except Exception:
148
- # Best-effort: package import must continue even if hook installation fails
149
- pass
150
-
151
- def refresh_host_package(package_name: str) -> None:
152
- """Re-apply wrappers for a registered package."""
153
- data = _REGISTERED.get(package_name.lower())
154
- if not data:
155
- return
156
- _apply_wrappers_for_loaded_modules(
157
- package_name,
158
- data["module_prefixes"],
159
- data["method_prefixes"],
160
- )
161
-
162
- def _apply_wrappers_for_loaded_modules(
163
- package_name: str,
164
- module_prefixes: Iterable[str],
165
- method_prefixes: Iterable[str],
166
- ) -> None:
167
- """Enhance already-imported modules so encode/decode helpers work immediately."""
168
- prefixes = _normalized((*module_prefixes, *method_prefixes))
169
- if not prefixes:
170
- return
171
-
172
- finder = LazyMetaPathFinder(package_name)
173
- for module_name, module in list(sys.modules.items()):
174
- if not isinstance(module_name, str) or module is None:
175
- continue
176
- if any(module_name.startswith(prefix) for prefix in prefixes):
177
- try:
178
- finder._enhance_classes_with_class_methods(module) # type: ignore[attr-defined]
179
- except Exception:
180
- continue
@@ -1,291 +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, 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 logger is None:
49
- import logging
50
- logger = logging.getLogger("xwlazy.lazy_installer.fallback")
51
- logger.addHandler(logging.NullHandler())
52
- if _log is None:
53
- _log = _get_log_event()
54
- if _log is None:
55
- # Simple fallback
56
- def _fallback_log(event, *args, **kwargs):
57
- pass
58
- _log = _fallback_log
59
-
60
- class AsyncInstallMixin:
61
- """Mixin for async installation operations."""
62
-
63
- def _ensure_async_loop(self) -> asyncio.AbstractEventLoop:
64
- """Ensure async event loop is running in background thread."""
65
- if self._async_loop is not None and self._async_loop.is_running(): # type: ignore[attr-defined]
66
- return self._async_loop # type: ignore[attr-defined]
67
-
68
- with self._lock: # type: ignore[attr-defined]
69
- if self._async_loop is None or not self._async_loop.is_running(): # type: ignore[attr-defined]
70
- loop_ready = threading.Event()
71
- loop_ref = [None]
72
-
73
- def _run_loop():
74
- loop = asyncio.new_event_loop()
75
- asyncio.set_event_loop(loop)
76
- loop_ref[0] = loop
77
- self._async_loop = loop # type: ignore[attr-defined]
78
- loop_ready.set()
79
- loop.run_forever()
80
-
81
- self._loop_thread = threading.Thread( # type: ignore[attr-defined]
82
- target=_run_loop,
83
- daemon=True,
84
- name=f"xwlazy-{self._package_name}-async" # type: ignore[attr-defined]
85
- )
86
- self._loop_thread.start() # type: ignore[attr-defined]
87
-
88
- if not loop_ready.wait(timeout=5.0):
89
- raise RuntimeError(f"Failed to start async loop for {self._package_name}") # type: ignore[attr-defined]
90
-
91
- return self._async_loop # type: ignore[attr-defined]
92
-
93
- def apply_manifest(self, manifest: Optional[PackageManifest]) -> None:
94
- """Apply manifest-driven configuration such as async installs."""
95
- env_override = _ENV_ASYNC_INSTALL
96
- # Force disable async install unless strictly overridden by env var (safety default)
97
- if not env_override:
98
- desired_async = False
99
- else:
100
- desired_async = True
101
- # desired_async = bool(env_override or (manifest and manifest.async_installs))
102
- desired_workers = _ENV_ASYNC_WORKERS or (manifest.async_workers if manifest else 1)
103
- desired_workers = max(1, desired_workers)
104
-
105
- with self._lock: # type: ignore[attr-defined]
106
- self._async_workers = desired_workers # type: ignore[attr-defined]
107
-
108
- if desired_async:
109
- self._ensure_async_loop()
110
- else:
111
- if self._async_loop is not None: # type: ignore[attr-defined]
112
- for task in list(self._async_tasks.values()): # type: ignore[attr-defined]
113
- if not task.done():
114
- task.cancel()
115
- self._async_tasks.clear() # type: ignore[attr-defined]
116
-
117
- self._async_enabled = desired_async # type: ignore[attr-defined]
118
-
119
- def is_async_enabled(self) -> bool:
120
- """Return True if async installers are enabled for this package."""
121
- return self._async_enabled # type: ignore[attr-defined]
122
-
123
- def ensure_async_install(self, module_name: str) -> Optional[AsyncInstallHandle]:
124
- """Schedule (or reuse) an async install job for module_name if async is enabled."""
125
- if not self._async_enabled: # type: ignore[attr-defined]
126
- return None
127
- return self.schedule_async_install(module_name)
128
-
129
- async def _get_package_size_mb(self, package_name: str) -> Optional[float]:
130
- """Get package size in MB by checking pip show or download size."""
131
- from ...common.services.install_async_utils import get_package_size_mb
132
- return await get_package_size_mb(package_name)
133
-
134
- async def _async_install_package(self, package_name: str, module_name: str) -> bool:
135
- """Async version of install_package using asyncio subprocess."""
136
- _ensure_logging_initialized()
137
- # SIZE_AWARE mode: Check package size before installing
138
- if self._mode == LazyInstallMode.SIZE_AWARE: # type: ignore[attr-defined]
139
- mode_config = LazyInstallConfig.get_mode_config(self._package_name) # type: ignore[attr-defined]
140
- threshold_mb = mode_config.large_package_threshold_mb if mode_config else 50.0
141
-
142
- size_mb = await self._get_package_size_mb(package_name)
143
- if size_mb is not None and size_mb >= threshold_mb:
144
- logger.warning(
145
- f"Package '{package_name}' is {size_mb:.1f}MB (>= {threshold_mb}MB threshold), "
146
- f"skipping installation in SIZE_AWARE mode"
147
- )
148
- print(
149
- f"[SIZE_AWARE] Skipping large package '{package_name}' "
150
- f"({size_mb:.1f}MB >= {threshold_mb}MB)"
151
- )
152
- self._failed_packages.add(package_name) # type: ignore[attr-defined]
153
- return False
154
-
155
- # Check cache first
156
- if self._install_from_cached_tree(package_name): # type: ignore[attr-defined]
157
- self._finalize_install_success(package_name, "cache-tree") # type: ignore[attr-defined]
158
- return True
159
-
160
- # Use asyncio subprocess for pip install
161
- try:
162
- policy_args = LazyInstallPolicy.get_pip_args(self._package_name) or [] # type: ignore[attr-defined]
163
- from ...common.services.install_async_utils import async_install_package
164
- success, error_msg = await async_install_package(package_name, policy_args)
165
-
166
- if success:
167
- self._finalize_install_success(package_name, "pip-async") # type: ignore[attr-defined]
168
-
169
- # For CLEAN mode, schedule async uninstall after completion
170
- if self._mode == LazyInstallMode.CLEAN: # type: ignore[attr-defined]
171
- asyncio.create_task(self._schedule_clean_uninstall(package_name))
172
-
173
- # For TEMPORARY mode, uninstall immediately after installation
174
- if self._mode == LazyInstallMode.TEMPORARY: # type: ignore[attr-defined]
175
- asyncio.create_task(self.uninstall_package_async(package_name, quiet=True))
176
-
177
- return True
178
- else:
179
- self._failed_packages.add(package_name) # type: ignore[attr-defined]
180
- return False
181
- except Exception as e:
182
- logger.error(f"Error in async install of {package_name}: {e}")
183
- self._failed_packages.add(package_name) # type: ignore[attr-defined]
184
- return False
185
-
186
- async def _schedule_clean_uninstall(self, package_name: str) -> None:
187
- """Schedule uninstall for CLEAN mode after a delay."""
188
- await asyncio.sleep(1.0)
189
- await self.uninstall_package_async(package_name, quiet=True)
190
-
191
- async def uninstall_package_async(self, package_name: str, quiet: bool = True) -> bool:
192
- """Uninstall a package asynchronously in quiet mode."""
193
- with self._lock: # type: ignore[attr-defined]
194
- if package_name not in self._installed_packages: # type: ignore[attr-defined]
195
- return True
196
-
197
- from ...common.services.install_async_utils import async_uninstall_package
198
- success = await async_uninstall_package(package_name, quiet)
199
-
200
- if success:
201
- with self._lock: # type: ignore[attr-defined]
202
- self._installed_packages.discard(package_name) # type: ignore[attr-defined]
203
-
204
- return success
205
-
206
- def schedule_async_install(self, module_name: str) -> Optional[AsyncInstallHandle]:
207
- """Schedule installation of a dependency in the background using asyncio."""
208
- _ensure_logging_initialized()
209
- if not self._async_enabled: # type: ignore[attr-defined]
210
- return None
211
-
212
- package_name = self._dependency_mapper.get_package_name(module_name) or module_name # type: ignore[attr-defined]
213
- if not package_name:
214
- return None
215
-
216
- with self._lock: # type: ignore[attr-defined]
217
- task = self._async_tasks.get(module_name) # type: ignore[attr-defined]
218
- if task is None or task.done():
219
- self._mark_module_missing(module_name) # type: ignore[attr-defined]
220
- loop = self._ensure_async_loop()
221
-
222
- async def _install_and_cleanup():
223
- try:
224
- result = await self._async_install_package(package_name, module_name)
225
- if result:
226
- self._clear_module_missing(module_name) # type: ignore[attr-defined]
227
- try:
228
- imported_module = importlib.import_module(module_name)
229
- if self._mode == LazyInstallMode.TEMPORARY: # type: ignore[attr-defined]
230
- asyncio.create_task(self.uninstall_package_async(package_name, quiet=True))
231
- except Exception:
232
- pass
233
- return result
234
- finally:
235
- with self._lock: # type: ignore[attr-defined]
236
- self._async_tasks.pop(module_name, None) # type: ignore[attr-defined]
237
-
238
- task = asyncio.run_coroutine_threadsafe(_install_and_cleanup(), loop)
239
- self._async_tasks[module_name] = task # type: ignore[attr-defined]
240
-
241
- return AsyncInstallHandle(task, module_name, package_name, self._package_name) # type: ignore[attr-defined]
242
-
243
- async def install_all_dependencies(self) -> None:
244
- """Install all dependencies from discovered requirements (FULL mode)."""
245
- _ensure_logging_initialized()
246
- if self._mode != LazyInstallMode.FULL: # type: ignore[attr-defined]
247
- return
248
-
249
- try:
250
- # Lazy import to avoid circular dependency
251
- from .discovery import get_lazy_discovery
252
- discovery = get_lazy_discovery()
253
- if discovery:
254
- all_deps = discovery.discover_all_dependencies()
255
- if not all_deps:
256
- return
257
-
258
- packages_to_install = [
259
- (import_name, package_name)
260
- for import_name, package_name in all_deps.items()
261
- if package_name not in self._installed_packages # type: ignore[attr-defined]
262
- ]
263
-
264
- if not packages_to_install:
265
- _log("install", f"All dependencies already installed for {self._package_name}") # type: ignore[attr-defined]
266
- return
267
-
268
- _log(
269
- "install",
270
- f"Installing {len(packages_to_install)} dependencies for {self._package_name} (FULL mode)"
271
- )
272
-
273
- batch_size = min(self._async_workers * 2, 10) # type: ignore[attr-defined]
274
- for i in range(0, len(packages_to_install), batch_size):
275
- batch = packages_to_install[i:i + batch_size]
276
- tasks = [
277
- self._async_install_package(package_name, import_name)
278
- for import_name, package_name in batch
279
- ]
280
- results = await asyncio.gather(*tasks, return_exceptions=True)
281
-
282
- for (import_name, package_name), result in zip(batch, results):
283
- if isinstance(result, Exception):
284
- logger.error(f"Failed to install {package_name}: {result}")
285
- elif result:
286
- _log("install", f"✓ Installed {package_name}")
287
-
288
- _log("install", f"Completed installing all dependencies for {self._package_name}") # type: ignore[attr-defined]
289
- except Exception as e:
290
- logger.warning(f"Failed to install all dependencies for {self._package_name}: {e}") # type: ignore[attr-defined]
291
-
@@ -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
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
-