exonware-xwlazy 0.1.0.11__py3-none-any.whl → 0.1.0.19__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 +22 -0
- exonware/xwlazy/__init__.py +0 -0
- exonware/xwlazy/common/__init__.py +47 -0
- exonware/xwlazy/common/base.py +58 -0
- exonware/xwlazy/common/cache.py +506 -0
- exonware/xwlazy/common/logger.py +268 -0
- exonware/xwlazy/common/services/__init__.py +72 -0
- exonware/xwlazy/common/services/dependency_mapper.py +234 -0
- exonware/xwlazy/common/services/install_async_utils.py +169 -0
- exonware/xwlazy/common/services/install_cache_utils.py +257 -0
- exonware/xwlazy/common/services/keyword_detection.py +292 -0
- exonware/xwlazy/common/services/spec_cache.py +173 -0
- exonware/xwlazy/common/strategies/__init__.py +28 -0
- exonware/xwlazy/common/strategies/caching_dict.py +45 -0
- exonware/xwlazy/common/strategies/caching_installation.py +89 -0
- exonware/xwlazy/common/strategies/caching_lfu.py +67 -0
- exonware/xwlazy/common/strategies/caching_lru.py +64 -0
- exonware/xwlazy/common/strategies/caching_multitier.py +60 -0
- exonware/xwlazy/common/strategies/caching_ttl.py +60 -0
- {xwlazy/lazy → exonware/xwlazy}/config.py +52 -20
- exonware/xwlazy/contracts.py +1410 -0
- exonware/xwlazy/defs.py +397 -0
- xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -8
- exonware/xwlazy/facade.py +1049 -0
- exonware/xwlazy/module/__init__.py +18 -0
- exonware/xwlazy/module/base.py +569 -0
- exonware/xwlazy/module/data.py +17 -0
- exonware/xwlazy/module/facade.py +247 -0
- exonware/xwlazy/module/importer_engine.py +2161 -0
- exonware/xwlazy/module/strategies/__init__.py +22 -0
- exonware/xwlazy/module/strategies/module_helper_lazy.py +94 -0
- exonware/xwlazy/module/strategies/module_helper_simple.py +66 -0
- exonware/xwlazy/module/strategies/module_manager_advanced.py +112 -0
- exonware/xwlazy/module/strategies/module_manager_simple.py +96 -0
- exonware/xwlazy/package/__init__.py +18 -0
- exonware/xwlazy/package/base.py +807 -0
- xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +62 -10
- exonware/xwlazy/package/data.py +17 -0
- exonware/xwlazy/package/facade.py +481 -0
- exonware/xwlazy/package/services/__init__.py +84 -0
- exonware/xwlazy/package/services/async_install_handle.py +89 -0
- exonware/xwlazy/package/services/config_manager.py +246 -0
- exonware/xwlazy/package/services/discovery.py +374 -0
- {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -16
- exonware/xwlazy/package/services/install_async.py +278 -0
- exonware/xwlazy/package/services/install_cache.py +146 -0
- exonware/xwlazy/package/services/install_interactive.py +60 -0
- exonware/xwlazy/package/services/install_policy.py +158 -0
- exonware/xwlazy/package/services/install_registry.py +56 -0
- exonware/xwlazy/package/services/install_result.py +17 -0
- exonware/xwlazy/package/services/install_sbom.py +154 -0
- exonware/xwlazy/package/services/install_utils.py +83 -0
- exonware/xwlazy/package/services/installer_engine.py +408 -0
- exonware/xwlazy/package/services/lazy_installer.py +720 -0
- {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +42 -25
- exonware/xwlazy/package/services/strategy_registry.py +188 -0
- exonware/xwlazy/package/strategies/__init__.py +57 -0
- exonware/xwlazy/package/strategies/package_discovery_file.py +130 -0
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +85 -0
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +102 -0
- exonware/xwlazy/package/strategies/package_execution_async.py +114 -0
- exonware/xwlazy/package/strategies/package_execution_cached.py +91 -0
- exonware/xwlazy/package/strategies/package_execution_pip.py +100 -0
- exonware/xwlazy/package/strategies/package_execution_wheel.py +107 -0
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +101 -0
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +106 -0
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +101 -0
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +58 -0
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +58 -0
- exonware/xwlazy/package/strategies/package_policy_permissive.py +47 -0
- exonware/xwlazy/package/strategies/package_timing_clean.py +68 -0
- exonware/xwlazy/package/strategies/package_timing_full.py +67 -0
- exonware/xwlazy/package/strategies/package_timing_smart.py +69 -0
- exonware/xwlazy/package/strategies/package_timing_temporary.py +67 -0
- exonware/xwlazy/runtime/__init__.py +18 -0
- exonware/xwlazy/runtime/adaptive_learner.py +131 -0
- exonware/xwlazy/runtime/base.py +276 -0
- exonware/xwlazy/runtime/facade.py +95 -0
- exonware/xwlazy/runtime/intelligent_selector.py +173 -0
- exonware/xwlazy/runtime/metrics.py +64 -0
- exonware/xwlazy/runtime/performance.py +39 -0
- exonware/xwlazy/version.py +2 -2
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/METADATA +86 -10
- exonware_xwlazy-0.1.0.19.dist-info/RECORD +87 -0
- exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
- xwlazy/__init__.py +0 -34
- xwlazy/lazy/__init__.py +0 -301
- xwlazy/lazy/bootstrap.py +0 -106
- xwlazy/lazy/lazy_base.py +0 -465
- xwlazy/lazy/lazy_contracts.py +0 -290
- xwlazy/lazy/lazy_core.py +0 -3727
- xwlazy/lazy/logging_utils.py +0 -194
- xwlazy/version.py +0 -77
- /xwlazy/lazy/lazy_state.py → /exonware/xwlazy/common/services/state_manager.py +0 -0
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Keyword-based detection for lazy installation.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to detect packages that opt-in to lazy loading
|
|
5
|
+
by including specific keywords in their metadata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from ..logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger("xwlazy.discovery.keyword")
|
|
16
|
+
|
|
17
|
+
# Global configuration
|
|
18
|
+
_KEYWORD_DETECTION_ENABLED: bool = True
|
|
19
|
+
_KEYWORD_TO_CHECK: str = "xwlazy-enabled"
|
|
20
|
+
_keyword_config_lock = threading.RLock()
|
|
21
|
+
|
|
22
|
+
# Detection cache
|
|
23
|
+
_lazy_detection_cache: dict[str, bool] = {}
|
|
24
|
+
_lazy_detection_lock = threading.RLock()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check_package_keywords(package_name: Optional[str] = None, keyword: Optional[str] = None) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Check if any installed package has the specified keyword in its metadata.
|
|
30
|
+
|
|
31
|
+
This allows packages to opt-in to lazy loading by adding:
|
|
32
|
+
[project]
|
|
33
|
+
keywords = ["xwlazy-enabled"]
|
|
34
|
+
|
|
35
|
+
in their pyproject.toml file. The keyword is stored in the package's
|
|
36
|
+
metadata when installed.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
package_name: The package name to check (or None to check all packages)
|
|
40
|
+
keyword: The keyword to look for (default: uses _KEYWORD_TO_CHECK)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if the keyword is found in any relevant package's metadata
|
|
44
|
+
"""
|
|
45
|
+
if not _KEYWORD_DETECTION_ENABLED:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
if sys.version_info < (3, 8):
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
from importlib import metadata
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
logger.debug(f"importlib.metadata unavailable for keyword detection: {exc}")
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
with _keyword_config_lock:
|
|
58
|
+
search_keyword = (keyword or _KEYWORD_TO_CHECK).lower()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
if package_name:
|
|
62
|
+
# Check specific package
|
|
63
|
+
try:
|
|
64
|
+
dist = metadata.distribution(package_name)
|
|
65
|
+
keywords = dist.metadata.get_all('Keywords', [])
|
|
66
|
+
if keywords:
|
|
67
|
+
# Keywords can be a single string or list
|
|
68
|
+
all_keywords = []
|
|
69
|
+
for kw in keywords:
|
|
70
|
+
if isinstance(kw, str):
|
|
71
|
+
# Split comma-separated keywords
|
|
72
|
+
all_keywords.extend(k.strip().lower() for k in kw.split(','))
|
|
73
|
+
else:
|
|
74
|
+
all_keywords.append(str(kw).lower())
|
|
75
|
+
|
|
76
|
+
if search_keyword in all_keywords:
|
|
77
|
+
logger.info(f"✅ Detected '{search_keyword}' keyword in package: {package_name}")
|
|
78
|
+
return True
|
|
79
|
+
except metadata.PackageNotFoundError:
|
|
80
|
+
return False
|
|
81
|
+
else:
|
|
82
|
+
# Check all installed packages
|
|
83
|
+
for dist in metadata.distributions():
|
|
84
|
+
try:
|
|
85
|
+
keywords = dist.metadata.get_all('Keywords', [])
|
|
86
|
+
if keywords:
|
|
87
|
+
all_keywords = []
|
|
88
|
+
for kw in keywords:
|
|
89
|
+
if isinstance(kw, str):
|
|
90
|
+
all_keywords.extend(k.strip().lower() for k in kw.split(','))
|
|
91
|
+
else:
|
|
92
|
+
all_keywords.append(str(kw).lower())
|
|
93
|
+
|
|
94
|
+
if search_keyword in all_keywords:
|
|
95
|
+
package_found = dist.metadata.get('Name', 'unknown')
|
|
96
|
+
logger.info(f"✅ Detected '{search_keyword}' keyword in package: {package_found}")
|
|
97
|
+
return True
|
|
98
|
+
except Exception:
|
|
99
|
+
continue
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
logger.debug(f"Failed to check package keywords: {exc}")
|
|
102
|
+
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _lazy_marker_installed() -> bool:
|
|
107
|
+
"""Check if the exonware-xwlazy marker package is installed."""
|
|
108
|
+
if sys.version_info < (3, 8):
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
from importlib import metadata
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
logger.debug(f"importlib.metadata unavailable for lazy detection: {exc}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
metadata.distribution("exonware-xwlazy")
|
|
119
|
+
logger.info("✅ Detected exonware-xwlazy marker package")
|
|
120
|
+
return True
|
|
121
|
+
except metadata.PackageNotFoundError:
|
|
122
|
+
logger.debug("❌ exonware-xwlazy marker package not installed")
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _lazy_env_override(package_name: str) -> Optional[bool]:
|
|
127
|
+
"""Check environment variable override for lazy installation."""
|
|
128
|
+
env_var = f"{package_name.upper()}_LAZY_INSTALL"
|
|
129
|
+
raw_value = os.environ.get(env_var)
|
|
130
|
+
if raw_value is None:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
normalized = raw_value.strip().lower()
|
|
134
|
+
if normalized in ("true", "1", "yes", "on"):
|
|
135
|
+
return True
|
|
136
|
+
if normalized in ("false", "0", "no", "off"):
|
|
137
|
+
return False
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _detect_meta_info_mode(package_name: str) -> Optional[str]:
|
|
142
|
+
"""
|
|
143
|
+
Detect lazy mode from package metadata keywords.
|
|
144
|
+
|
|
145
|
+
Checks for keywords like:
|
|
146
|
+
- xwlazy-load-install-uninstall (clean mode)
|
|
147
|
+
- xwlazy-lite (lite mode)
|
|
148
|
+
- xwlazy-smart (smart mode)
|
|
149
|
+
- xwlazy-full (full mode)
|
|
150
|
+
- xwlazy-auto (auto mode)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Mode string or None if not found
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
import importlib.metadata
|
|
157
|
+
try:
|
|
158
|
+
dist = importlib.metadata.distribution(package_name)
|
|
159
|
+
keywords = dist.metadata.get_all("Keywords", [])
|
|
160
|
+
if not keywords:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
keyword_str = " ".join(keywords).lower()
|
|
164
|
+
|
|
165
|
+
if "xwlazy-load-install-uninstall" in keyword_str:
|
|
166
|
+
return "clean"
|
|
167
|
+
if "xwlazy-lite" in keyword_str:
|
|
168
|
+
return "lite"
|
|
169
|
+
if "xwlazy-smart" in keyword_str:
|
|
170
|
+
return "smart"
|
|
171
|
+
if "xwlazy-full" in keyword_str:
|
|
172
|
+
return "full"
|
|
173
|
+
if "xwlazy-auto" in keyword_str:
|
|
174
|
+
return "auto"
|
|
175
|
+
except importlib.metadata.PackageNotFoundError:
|
|
176
|
+
return None
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def enable_keyword_detection(enabled: bool = True, keyword: Optional[str] = None) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Enable/disable keyword-based auto-detection of lazy loading.
|
|
185
|
+
|
|
186
|
+
When enabled, xwlazy will check installed packages for a keyword
|
|
187
|
+
(default: "xwlazy-enabled") in their metadata. Packages can opt-in
|
|
188
|
+
by adding the keyword to their pyproject.toml:
|
|
189
|
+
|
|
190
|
+
[project]
|
|
191
|
+
keywords = ["xwlazy-enabled"]
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
enabled: Whether to enable keyword detection (default: True)
|
|
195
|
+
keyword: Custom keyword to check (default: "xwlazy-enabled")
|
|
196
|
+
"""
|
|
197
|
+
global _KEYWORD_DETECTION_ENABLED, _KEYWORD_TO_CHECK
|
|
198
|
+
with _keyword_config_lock:
|
|
199
|
+
_KEYWORD_DETECTION_ENABLED = enabled
|
|
200
|
+
if keyword is not None:
|
|
201
|
+
_KEYWORD_TO_CHECK = keyword
|
|
202
|
+
# Clear cache to force re-detection
|
|
203
|
+
with _lazy_detection_lock:
|
|
204
|
+
_lazy_detection_cache.clear()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def is_keyword_detection_enabled() -> bool:
|
|
208
|
+
"""Return whether keyword-based detection is enabled."""
|
|
209
|
+
with _keyword_config_lock:
|
|
210
|
+
return _KEYWORD_DETECTION_ENABLED
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_keyword_detection_keyword() -> str:
|
|
214
|
+
"""Get the keyword currently being checked for auto-detection."""
|
|
215
|
+
with _keyword_config_lock:
|
|
216
|
+
return _KEYWORD_TO_CHECK
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def check_package_keywords(package_name: Optional[str] = None, keyword: Optional[str] = None) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
Check if a package (or any package) has the specified keyword in its metadata.
|
|
222
|
+
|
|
223
|
+
This is the public API for the keyword detection functionality.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
package_name: The package name to check (or None to check all packages)
|
|
227
|
+
keyword: The keyword to look for (default: uses configured keyword)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if the keyword is found in the package's metadata
|
|
231
|
+
"""
|
|
232
|
+
return _check_package_keywords(package_name, keyword)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _detect_lazy_installation(package_name: str) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
Detect if lazy installation should be enabled for a package.
|
|
238
|
+
|
|
239
|
+
This function checks multiple sources in order:
|
|
240
|
+
1. Environment variable override
|
|
241
|
+
2. Manual state (from state manager)
|
|
242
|
+
3. Cached auto state
|
|
243
|
+
4. Marker package detection
|
|
244
|
+
5. Keyword detection
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
package_name: The package name to check
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if lazy installation should be enabled
|
|
251
|
+
"""
|
|
252
|
+
with _lazy_detection_lock:
|
|
253
|
+
cached = _lazy_detection_cache.get(package_name)
|
|
254
|
+
if cached is not None:
|
|
255
|
+
return cached
|
|
256
|
+
|
|
257
|
+
env_override = _lazy_env_override(package_name)
|
|
258
|
+
if env_override is not None:
|
|
259
|
+
with _lazy_detection_lock:
|
|
260
|
+
_lazy_detection_cache[package_name] = env_override
|
|
261
|
+
return env_override
|
|
262
|
+
|
|
263
|
+
from .state_manager import LazyStateManager
|
|
264
|
+
state_manager = LazyStateManager(package_name)
|
|
265
|
+
manual_state = state_manager.get_manual_state()
|
|
266
|
+
if manual_state is not None:
|
|
267
|
+
with _lazy_detection_lock:
|
|
268
|
+
_lazy_detection_cache[package_name] = manual_state
|
|
269
|
+
return manual_state
|
|
270
|
+
|
|
271
|
+
cached_state = state_manager.get_cached_auto_state()
|
|
272
|
+
if cached_state is not None:
|
|
273
|
+
with _lazy_detection_lock:
|
|
274
|
+
_lazy_detection_cache[package_name] = cached_state
|
|
275
|
+
return cached_state
|
|
276
|
+
|
|
277
|
+
# Check marker package first (existing behavior)
|
|
278
|
+
marker_detected = _lazy_marker_installed()
|
|
279
|
+
|
|
280
|
+
# Also check for keyword in package metadata (new feature)
|
|
281
|
+
keyword_detected = _check_package_keywords(package_name)
|
|
282
|
+
|
|
283
|
+
# Enable if either marker package OR keyword is found
|
|
284
|
+
detected = marker_detected or keyword_detected
|
|
285
|
+
|
|
286
|
+
state_manager.set_auto_state(detected)
|
|
287
|
+
|
|
288
|
+
with _lazy_detection_lock:
|
|
289
|
+
_lazy_detection_cache[package_name] = detected
|
|
290
|
+
|
|
291
|
+
return detected
|
|
292
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
#exonware/xwlazy/src/exonware/xwlazy/discovery/spec_cache.py
|
|
3
|
+
|
|
4
|
+
Spec cache utilities for module specification caching.
|
|
5
|
+
|
|
6
|
+
Company: eXonware.com
|
|
7
|
+
Author: Eng. Muhammad AlShehri
|
|
8
|
+
Email: connect@exonware.com
|
|
9
|
+
Version: 0.1.0.19
|
|
10
|
+
Generation Date: 10-Oct-2025
|
|
11
|
+
|
|
12
|
+
This module provides multi-level caching (L1: memory, L2: disk) for module specs
|
|
13
|
+
to optimize import performance.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
import threading
|
|
20
|
+
import importlib
|
|
21
|
+
import importlib.machinery
|
|
22
|
+
import importlib.util
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional, Tuple
|
|
25
|
+
from collections import OrderedDict
|
|
26
|
+
from functools import lru_cache
|
|
27
|
+
|
|
28
|
+
# Environment variables
|
|
29
|
+
_SPEC_CACHE_MAX = int(os.environ.get("XWLAZY_SPEC_CACHE_MAX", "512") or 512)
|
|
30
|
+
_SPEC_CACHE_TTL = float(os.environ.get("XWLAZY_SPEC_CACHE_TTL", "60") or 60.0)
|
|
31
|
+
|
|
32
|
+
# Cache storage
|
|
33
|
+
_spec_cache_lock = threading.RLock()
|
|
34
|
+
_spec_cache: OrderedDict[str, Tuple[importlib.machinery.ModuleSpec, float]] = OrderedDict()
|
|
35
|
+
|
|
36
|
+
# Multi-level cache: L1 (in-memory) + L2 (disk)
|
|
37
|
+
_CACHE_L2_DIR = Path(
|
|
38
|
+
os.environ.get(
|
|
39
|
+
"XWLAZY_CACHE_DIR",
|
|
40
|
+
os.path.join(os.path.expanduser("~"), ".xwlazy", "cache"),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
_CACHE_L2_DIR.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
# Stdlib module set
|
|
46
|
+
try:
|
|
47
|
+
_STDLIB_MODULE_SET: set[str] = set(sys.stdlib_module_names) # type: ignore[attr-defined]
|
|
48
|
+
except AttributeError:
|
|
49
|
+
_STDLIB_MODULE_SET = set()
|
|
50
|
+
_STDLIB_MODULE_SET.update(sys.builtin_module_names)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@lru_cache(maxsize=1024)
|
|
54
|
+
def _cached_stdlib_check(module_name: str) -> bool:
|
|
55
|
+
"""Check if module is part of stdlib or built-in modules."""
|
|
56
|
+
try:
|
|
57
|
+
spec = importlib.util.find_spec(module_name)
|
|
58
|
+
if spec is None:
|
|
59
|
+
return False
|
|
60
|
+
if spec.origin in ("built-in", None):
|
|
61
|
+
return True
|
|
62
|
+
origin = spec.origin or ""
|
|
63
|
+
return (
|
|
64
|
+
"python" in origin.lower()
|
|
65
|
+
and "site-packages" not in origin.lower()
|
|
66
|
+
and "dist-packages" not in origin.lower()
|
|
67
|
+
)
|
|
68
|
+
except Exception:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _spec_cache_prune_locked(now: Optional[float] = None) -> None:
|
|
73
|
+
"""Prune expired entries from spec cache (must be called with lock held)."""
|
|
74
|
+
if not _spec_cache:
|
|
75
|
+
return
|
|
76
|
+
current = now or time.monotonic()
|
|
77
|
+
while _spec_cache:
|
|
78
|
+
fullname, (_, ts) = next(iter(_spec_cache.items()))
|
|
79
|
+
if current - ts <= _SPEC_CACHE_TTL and len(_spec_cache) <= _SPEC_CACHE_MAX:
|
|
80
|
+
break
|
|
81
|
+
_spec_cache.popitem(last=False)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _spec_cache_get(fullname: str) -> Optional[importlib.machinery.ModuleSpec]:
|
|
85
|
+
"""Get spec from multi-level cache (L1: memory, L2: disk)."""
|
|
86
|
+
with _spec_cache_lock:
|
|
87
|
+
_spec_cache_prune_locked()
|
|
88
|
+
entry = _spec_cache.get(fullname)
|
|
89
|
+
if entry is not None:
|
|
90
|
+
spec, _ = entry
|
|
91
|
+
_spec_cache.move_to_end(fullname)
|
|
92
|
+
return spec
|
|
93
|
+
|
|
94
|
+
# L2 cache: Check disk cache
|
|
95
|
+
try:
|
|
96
|
+
cache_file = _CACHE_L2_DIR / f"{fullname.replace('.', '_')}.spec"
|
|
97
|
+
if cache_file.exists():
|
|
98
|
+
mtime = cache_file.stat().st_mtime
|
|
99
|
+
age = time.time() - mtime
|
|
100
|
+
if age < _SPEC_CACHE_TTL:
|
|
101
|
+
try:
|
|
102
|
+
import pickle
|
|
103
|
+
with open(cache_file, 'rb') as f:
|
|
104
|
+
spec = pickle.load(f)
|
|
105
|
+
# Promote to L1 cache
|
|
106
|
+
_spec_cache[fullname] = (spec, time.monotonic())
|
|
107
|
+
_spec_cache.move_to_end(fullname)
|
|
108
|
+
return spec
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _spec_cache_put(fullname: str, spec: Optional[importlib.machinery.ModuleSpec]) -> None:
|
|
118
|
+
"""Put spec in multi-level cache (L1: memory, L2: disk)."""
|
|
119
|
+
if spec is None:
|
|
120
|
+
return
|
|
121
|
+
with _spec_cache_lock:
|
|
122
|
+
# L1 cache: In-memory
|
|
123
|
+
_spec_cache[fullname] = (spec, time.monotonic())
|
|
124
|
+
_spec_cache.move_to_end(fullname)
|
|
125
|
+
_spec_cache_prune_locked()
|
|
126
|
+
|
|
127
|
+
# L2 cache: Disk (async, non-blocking)
|
|
128
|
+
try:
|
|
129
|
+
cache_file = _CACHE_L2_DIR / f"{fullname.replace('.', '_')}.spec"
|
|
130
|
+
import pickle
|
|
131
|
+
# Use protocol 5 for better performance
|
|
132
|
+
with open(cache_file, 'wb') as f:
|
|
133
|
+
pickle.dump(spec, f, protocol=5)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass # Fail silently for disk cache
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _spec_cache_clear(fullname: Optional[str] = None) -> None:
|
|
139
|
+
"""Clear spec cache entries."""
|
|
140
|
+
with _spec_cache_lock:
|
|
141
|
+
if fullname is None:
|
|
142
|
+
_spec_cache.clear()
|
|
143
|
+
else:
|
|
144
|
+
_spec_cache.pop(fullname, None)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _cache_spec_if_missing(fullname: str) -> None:
|
|
148
|
+
"""Ensure a ModuleSpec is cached for a known-good module."""
|
|
149
|
+
if _spec_cache_get(fullname):
|
|
150
|
+
return
|
|
151
|
+
try:
|
|
152
|
+
spec = importlib.util.find_spec(fullname)
|
|
153
|
+
except Exception:
|
|
154
|
+
spec = None
|
|
155
|
+
if spec is not None:
|
|
156
|
+
_spec_cache_put(fullname, spec)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_stdlib_module_set() -> set[str]:
|
|
160
|
+
"""Get the set of stdlib module names."""
|
|
161
|
+
return _STDLIB_MODULE_SET.copy()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = [
|
|
165
|
+
'_cached_stdlib_check',
|
|
166
|
+
'_spec_cache_get',
|
|
167
|
+
'_spec_cache_put',
|
|
168
|
+
'_spec_cache_clear',
|
|
169
|
+
'_spec_cache_prune_locked',
|
|
170
|
+
'_cache_spec_if_missing',
|
|
171
|
+
'get_stdlib_module_set',
|
|
172
|
+
]
|
|
173
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common Caching Strategies - Shared by modules and packages.
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
Version: 0.1.0.19
|
|
8
|
+
Generation Date: 15-Nov-2025
|
|
9
|
+
|
|
10
|
+
Generic caching strategies that work with ANY data type.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .caching_dict import DictCache
|
|
14
|
+
from .caching_lru import LRUCache
|
|
15
|
+
from .caching_lfu import LFUCache
|
|
16
|
+
from .caching_ttl import TTLCache
|
|
17
|
+
from .caching_multitier import MultiTierCacheStrategy
|
|
18
|
+
from .caching_installation import InstallationCacheWrapper
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
'DictCache',
|
|
22
|
+
'LRUCache',
|
|
23
|
+
'LFUCache',
|
|
24
|
+
'TTLCache',
|
|
25
|
+
'MultiTierCacheStrategy',
|
|
26
|
+
'InstallationCacheWrapper',
|
|
27
|
+
]
|
|
28
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dict Cache Strategy - Simple dictionary-based caching.
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
Version: 0.1.0.19
|
|
8
|
+
Generation Date: 15-Nov-2025
|
|
9
|
+
|
|
10
|
+
Simple dict-based cache implementation.
|
|
11
|
+
Works with ANY data type (modules, packages, etc.).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Dict, Optional, Any
|
|
15
|
+
from ...common.base import ACachingStrategy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DictCache(ACachingStrategy):
|
|
19
|
+
"""
|
|
20
|
+
Simple dictionary-based cache.
|
|
21
|
+
|
|
22
|
+
No eviction policy - grows unbounded.
|
|
23
|
+
Use for small applications or when memory is not a concern.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize dict cache."""
|
|
28
|
+
self._cache: Dict[str, Any] = {}
|
|
29
|
+
|
|
30
|
+
def get(self, key: str) -> Optional[Any]:
|
|
31
|
+
"""Get value from cache."""
|
|
32
|
+
return self._cache.get(key)
|
|
33
|
+
|
|
34
|
+
def set(self, key: str, value: Any) -> None:
|
|
35
|
+
"""Set value in cache."""
|
|
36
|
+
self._cache[key] = value
|
|
37
|
+
|
|
38
|
+
def invalidate(self, key: str) -> None:
|
|
39
|
+
"""Invalidate cached value."""
|
|
40
|
+
self._cache.pop(key, None)
|
|
41
|
+
|
|
42
|
+
def clear(self) -> None:
|
|
43
|
+
"""Clear all cached values."""
|
|
44
|
+
self._cache.clear()
|
|
45
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Installation Cache Strategy - Wrapper for existing InstallationCache.
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
Version: 0.1.0.19
|
|
8
|
+
Generation Date: 15-Nov-2025
|
|
9
|
+
|
|
10
|
+
Wraps existing InstallationCache to implement ICachingStrategy interface.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Optional, Any
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from ..cache import InstallationCache
|
|
16
|
+
from ..base import ACachingStrategy
|
|
17
|
+
from ...package.data import PackageData
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InstallationCacheWrapper(ACachingStrategy):
|
|
21
|
+
"""
|
|
22
|
+
Installation cache strategy wrapper.
|
|
23
|
+
|
|
24
|
+
Wraps existing InstallationCache to implement ICachingStrategy interface.
|
|
25
|
+
Used for package installation status caching.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, cache_file: Optional[Path] = None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize installation cache wrapper.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
cache_file: Optional path to cache file
|
|
34
|
+
"""
|
|
35
|
+
self._cache = InstallationCache(cache_file)
|
|
36
|
+
|
|
37
|
+
def get(self, key: str) -> Optional[Any]:
|
|
38
|
+
"""
|
|
39
|
+
Get package from cache.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
key: Package name
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
PackageData if found, None otherwise
|
|
46
|
+
"""
|
|
47
|
+
if self._cache.is_installed(key):
|
|
48
|
+
version = self._cache.get_version(key)
|
|
49
|
+
return PackageData(
|
|
50
|
+
name=key,
|
|
51
|
+
installed=True,
|
|
52
|
+
version=version
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def set(self, key: str, value: Any) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Cache a package.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
key: Package name
|
|
62
|
+
value: PackageData or dict with installed/version info
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(value, PackageData):
|
|
65
|
+
if value.installed:
|
|
66
|
+
self._cache.mark_installed(key, value.version)
|
|
67
|
+
else:
|
|
68
|
+
self._cache.mark_uninstalled(key)
|
|
69
|
+
elif isinstance(value, dict):
|
|
70
|
+
if value.get('installed', False):
|
|
71
|
+
self._cache.mark_installed(key, value.get('version'))
|
|
72
|
+
else:
|
|
73
|
+
self._cache.mark_uninstalled(key)
|
|
74
|
+
|
|
75
|
+
def invalidate(self, key: str) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Invalidate cached package.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
key: Package name
|
|
81
|
+
"""
|
|
82
|
+
self._cache.mark_uninstalled(key)
|
|
83
|
+
|
|
84
|
+
def clear(self) -> None:
|
|
85
|
+
"""Clear all cached packages."""
|
|
86
|
+
# InstallationCache doesn't have clear, so we'd need to extend it
|
|
87
|
+
# For now, just mark all as uninstalled (would need cache iteration)
|
|
88
|
+
pass
|
|
89
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LFU Cache Strategy - Least Frequently Used eviction.
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
Version: 0.1.0.19
|
|
8
|
+
Generation Date: 15-Nov-2025
|
|
9
|
+
|
|
10
|
+
LFU cache implementation with size limit.
|
|
11
|
+
Works with ANY data type (modules, packages, etc.).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Dict, Optional, Any
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from ...common.base import ACachingStrategy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LFUCache(ACachingStrategy):
|
|
20
|
+
"""
|
|
21
|
+
LFU (Least Frequently Used) cache with size limit.
|
|
22
|
+
|
|
23
|
+
Evicts least frequently accessed items when cache is full.
|
|
24
|
+
Good for access pattern-based caching.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, max_size: int = 1000):
|
|
28
|
+
"""
|
|
29
|
+
Initialize LFU cache.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
max_size: Maximum number of items in cache
|
|
33
|
+
"""
|
|
34
|
+
self._cache: Dict[str, Any] = {}
|
|
35
|
+
self._freq: Counter[str] = Counter()
|
|
36
|
+
self._max_size = max_size
|
|
37
|
+
|
|
38
|
+
def get(self, key: str) -> Optional[Any]:
|
|
39
|
+
"""Get value from cache (increments frequency)."""
|
|
40
|
+
if key in self._cache:
|
|
41
|
+
self._freq[key] += 1
|
|
42
|
+
return self._cache[key]
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def set(self, key: str, value: Any) -> None:
|
|
46
|
+
"""Set value in cache (evicts least frequent if full)."""
|
|
47
|
+
if key not in self._cache and len(self._cache) >= self._max_size:
|
|
48
|
+
# Evict least frequent
|
|
49
|
+
if self._freq:
|
|
50
|
+
least_frequent = min(self._freq.items(), key=lambda x: x[1])[0]
|
|
51
|
+
self._cache.pop(least_frequent, None)
|
|
52
|
+
self._freq.pop(least_frequent, None)
|
|
53
|
+
|
|
54
|
+
self._cache[key] = value
|
|
55
|
+
if key not in self._freq:
|
|
56
|
+
self._freq[key] = 0
|
|
57
|
+
|
|
58
|
+
def invalidate(self, key: str) -> None:
|
|
59
|
+
"""Invalidate cached value."""
|
|
60
|
+
self._cache.pop(key, None)
|
|
61
|
+
self._freq.pop(key, None)
|
|
62
|
+
|
|
63
|
+
def clear(self) -> None:
|
|
64
|
+
"""Clear all cached values."""
|
|
65
|
+
self._cache.clear()
|
|
66
|
+
self._freq.clear()
|
|
67
|
+
|