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.
- exonware/__init__.py +85 -34
- exonware/xwlazy/version.py +5 -5
- exonware/xwlazy.py +2546 -0
- exonware/xwlazy_external_libs.toml +716 -0
- {exonware_xwlazy-0.1.0.23.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/METADATA +5 -1
- exonware_xwlazy-1.0.1.2.dist-info/RECORD +8 -0
- exonware/xwlazy/__init__.py +0 -379
- exonware/xwlazy/common/__init__.py +0 -55
- exonware/xwlazy/common/base.py +0 -65
- exonware/xwlazy/common/cache.py +0 -504
- exonware/xwlazy/common/logger.py +0 -257
- exonware/xwlazy/common/services/__init__.py +0 -72
- exonware/xwlazy/common/services/dependency_mapper.py +0 -250
- exonware/xwlazy/common/services/install_async_utils.py +0 -170
- exonware/xwlazy/common/services/install_cache_utils.py +0 -245
- exonware/xwlazy/common/services/keyword_detection.py +0 -283
- exonware/xwlazy/common/services/spec_cache.py +0 -165
- exonware/xwlazy/common/services/state_manager.py +0 -84
- exonware/xwlazy/common/strategies/__init__.py +0 -28
- exonware/xwlazy/common/strategies/caching_dict.py +0 -44
- exonware/xwlazy/common/strategies/caching_installation.py +0 -88
- exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
- exonware/xwlazy/common/strategies/caching_lru.py +0 -63
- exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
- exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
- exonware/xwlazy/common/utils.py +0 -142
- exonware/xwlazy/config.py +0 -193
- exonware/xwlazy/contracts.py +0 -1533
- exonware/xwlazy/defs.py +0 -378
- exonware/xwlazy/errors.py +0 -276
- exonware/xwlazy/facade.py +0 -1137
- exonware/xwlazy/host/__init__.py +0 -8
- exonware/xwlazy/host/conf.py +0 -16
- exonware/xwlazy/module/__init__.py +0 -18
- exonware/xwlazy/module/base.py +0 -622
- exonware/xwlazy/module/data.py +0 -17
- exonware/xwlazy/module/facade.py +0 -246
- exonware/xwlazy/module/importer_engine.py +0 -2964
- exonware/xwlazy/module/partial_module_detector.py +0 -275
- exonware/xwlazy/module/strategies/__init__.py +0 -22
- exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
- exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
- exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
- exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
- exonware/xwlazy/package/__init__.py +0 -18
- exonware/xwlazy/package/base.py +0 -863
- exonware/xwlazy/package/conf.py +0 -324
- exonware/xwlazy/package/data.py +0 -17
- exonware/xwlazy/package/facade.py +0 -480
- exonware/xwlazy/package/services/__init__.py +0 -84
- exonware/xwlazy/package/services/async_install_handle.py +0 -87
- exonware/xwlazy/package/services/config_manager.py +0 -249
- exonware/xwlazy/package/services/discovery.py +0 -435
- exonware/xwlazy/package/services/host_packages.py +0 -180
- exonware/xwlazy/package/services/install_async.py +0 -291
- exonware/xwlazy/package/services/install_cache.py +0 -145
- exonware/xwlazy/package/services/install_interactive.py +0 -59
- exonware/xwlazy/package/services/install_policy.py +0 -156
- exonware/xwlazy/package/services/install_registry.py +0 -54
- exonware/xwlazy/package/services/install_result.py +0 -17
- exonware/xwlazy/package/services/install_sbom.py +0 -153
- exonware/xwlazy/package/services/install_utils.py +0 -79
- exonware/xwlazy/package/services/installer_engine.py +0 -406
- exonware/xwlazy/package/services/lazy_installer.py +0 -803
- exonware/xwlazy/package/services/manifest.py +0 -503
- exonware/xwlazy/package/services/strategy_registry.py +0 -324
- exonware/xwlazy/package/strategies/__init__.py +0 -57
- exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
- exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
- exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
- exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
- exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
- exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
- exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
- exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
- exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
- exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
- exonware/xwlazy/runtime/__init__.py +0 -18
- exonware/xwlazy/runtime/adaptive_learner.py +0 -129
- exonware/xwlazy/runtime/base.py +0 -274
- exonware/xwlazy/runtime/facade.py +0 -94
- exonware/xwlazy/runtime/intelligent_selector.py +0 -170
- exonware/xwlazy/runtime/metrics.py +0 -60
- exonware/xwlazy/runtime/performance.py +0 -37
- exonware_xwlazy-0.1.0.23.dist-info/RECORD +0 -93
- xwlazy/__init__.py +0 -14
- xwlazy/lazy.py +0 -30
- {exonware_xwlazy-0.1.0.23.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/WHEEL +0 -0
- {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
|
-
|