exonware-xwlazy 0.1.0.10__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.
Files changed (89) hide show
  1. exonware/__init__.py +22 -0
  2. exonware/xwlazy/__init__.py +0 -0
  3. exonware/xwlazy/common/__init__.py +47 -0
  4. exonware/xwlazy/common/base.py +58 -0
  5. exonware/xwlazy/common/cache.py +506 -0
  6. exonware/xwlazy/common/logger.py +268 -0
  7. exonware/xwlazy/common/services/__init__.py +72 -0
  8. exonware/xwlazy/common/services/dependency_mapper.py +234 -0
  9. exonware/xwlazy/common/services/install_async_utils.py +169 -0
  10. exonware/xwlazy/common/services/install_cache_utils.py +257 -0
  11. exonware/xwlazy/common/services/keyword_detection.py +292 -0
  12. exonware/xwlazy/common/services/spec_cache.py +173 -0
  13. exonware/xwlazy/common/services/state_manager.py +86 -0
  14. exonware/xwlazy/common/strategies/__init__.py +28 -0
  15. exonware/xwlazy/common/strategies/caching_dict.py +45 -0
  16. exonware/xwlazy/common/strategies/caching_installation.py +89 -0
  17. exonware/xwlazy/common/strategies/caching_lfu.py +67 -0
  18. exonware/xwlazy/common/strategies/caching_lru.py +64 -0
  19. exonware/xwlazy/common/strategies/caching_multitier.py +60 -0
  20. exonware/xwlazy/common/strategies/caching_ttl.py +60 -0
  21. exonware/xwlazy/config.py +195 -0
  22. exonware/xwlazy/contracts.py +1410 -0
  23. exonware/xwlazy/defs.py +397 -0
  24. exonware/xwlazy/errors.py +284 -0
  25. exonware/xwlazy/facade.py +1049 -0
  26. exonware/xwlazy/module/__init__.py +18 -0
  27. exonware/xwlazy/module/base.py +569 -0
  28. exonware/xwlazy/module/data.py +17 -0
  29. exonware/xwlazy/module/facade.py +247 -0
  30. exonware/xwlazy/module/importer_engine.py +2161 -0
  31. exonware/xwlazy/module/strategies/__init__.py +22 -0
  32. exonware/xwlazy/module/strategies/module_helper_lazy.py +94 -0
  33. exonware/xwlazy/module/strategies/module_helper_simple.py +66 -0
  34. exonware/xwlazy/module/strategies/module_manager_advanced.py +112 -0
  35. exonware/xwlazy/module/strategies/module_manager_simple.py +96 -0
  36. exonware/xwlazy/package/__init__.py +18 -0
  37. exonware/xwlazy/package/base.py +807 -0
  38. exonware/xwlazy/package/conf.py +331 -0
  39. exonware/xwlazy/package/data.py +17 -0
  40. exonware/xwlazy/package/facade.py +481 -0
  41. exonware/xwlazy/package/services/__init__.py +84 -0
  42. exonware/xwlazy/package/services/async_install_handle.py +89 -0
  43. exonware/xwlazy/package/services/config_manager.py +246 -0
  44. exonware/xwlazy/package/services/discovery.py +374 -0
  45. exonware/xwlazy/package/services/host_packages.py +149 -0
  46. exonware/xwlazy/package/services/install_async.py +278 -0
  47. exonware/xwlazy/package/services/install_cache.py +146 -0
  48. exonware/xwlazy/package/services/install_interactive.py +60 -0
  49. exonware/xwlazy/package/services/install_policy.py +158 -0
  50. exonware/xwlazy/package/services/install_registry.py +56 -0
  51. exonware/xwlazy/package/services/install_result.py +17 -0
  52. exonware/xwlazy/package/services/install_sbom.py +154 -0
  53. exonware/xwlazy/package/services/install_utils.py +83 -0
  54. exonware/xwlazy/package/services/installer_engine.py +408 -0
  55. exonware/xwlazy/package/services/lazy_installer.py +720 -0
  56. exonware/xwlazy/package/services/manifest.py +506 -0
  57. exonware/xwlazy/package/services/strategy_registry.py +188 -0
  58. exonware/xwlazy/package/strategies/__init__.py +57 -0
  59. exonware/xwlazy/package/strategies/package_discovery_file.py +130 -0
  60. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +85 -0
  61. exonware/xwlazy/package/strategies/package_discovery_manifest.py +102 -0
  62. exonware/xwlazy/package/strategies/package_execution_async.py +114 -0
  63. exonware/xwlazy/package/strategies/package_execution_cached.py +91 -0
  64. exonware/xwlazy/package/strategies/package_execution_pip.py +100 -0
  65. exonware/xwlazy/package/strategies/package_execution_wheel.py +107 -0
  66. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +101 -0
  67. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +106 -0
  68. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +101 -0
  69. exonware/xwlazy/package/strategies/package_policy_allow_list.py +58 -0
  70. exonware/xwlazy/package/strategies/package_policy_deny_list.py +58 -0
  71. exonware/xwlazy/package/strategies/package_policy_permissive.py +47 -0
  72. exonware/xwlazy/package/strategies/package_timing_clean.py +68 -0
  73. exonware/xwlazy/package/strategies/package_timing_full.py +67 -0
  74. exonware/xwlazy/package/strategies/package_timing_smart.py +69 -0
  75. exonware/xwlazy/package/strategies/package_timing_temporary.py +67 -0
  76. exonware/xwlazy/runtime/__init__.py +18 -0
  77. exonware/xwlazy/runtime/adaptive_learner.py +131 -0
  78. exonware/xwlazy/runtime/base.py +276 -0
  79. exonware/xwlazy/runtime/facade.py +95 -0
  80. exonware/xwlazy/runtime/intelligent_selector.py +173 -0
  81. exonware/xwlazy/runtime/metrics.py +64 -0
  82. exonware/xwlazy/runtime/performance.py +39 -0
  83. exonware/xwlazy/version.py +2 -2
  84. exonware_xwlazy-0.1.0.19.dist-info/METADATA +456 -0
  85. exonware_xwlazy-0.1.0.19.dist-info/RECORD +87 -0
  86. exonware_xwlazy-0.1.0.10.dist-info/METADATA +0 -0
  87. exonware_xwlazy-0.1.0.10.dist-info/RECORD +0 -6
  88. {exonware_xwlazy-0.1.0.10.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/WHEEL +0 -0
  89. {exonware_xwlazy-0.1.0.10.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,86 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Dict, Optional
7
+
8
+
9
+ def _get_base_config_dir() -> Path:
10
+ """Determine a cross-platform directory for storing lazy configuration."""
11
+ if os.name == "nt":
12
+ appdata = os.getenv("APPDATA")
13
+ if appdata:
14
+ return Path(appdata) / "exonware" / "lazy"
15
+ return Path.home() / "AppData" / "Roaming" / "exonware" / "lazy"
16
+
17
+ # POSIX-style
18
+ xdg_config = os.getenv("XDG_CONFIG_HOME")
19
+ if xdg_config:
20
+ return Path(xdg_config) / "exonware" / "lazy"
21
+ return Path.home() / ".config" / "exonware" / "lazy"
22
+
23
+
24
+ class LazyStateManager:
25
+ """Persist and retrieve lazy installation state."""
26
+
27
+ def __init__(self, package_name: str) -> None:
28
+ self._package = package_name.lower()
29
+ self._state_path = _get_base_config_dir() / "state.json"
30
+ self._state: Dict[str, Dict[str, bool]] = self._load_state()
31
+
32
+ # --------------------------------------------------------------------- #
33
+ # Persistence helpers
34
+ # --------------------------------------------------------------------- #
35
+ def _load_state(self) -> Dict[str, Dict[str, bool]]:
36
+ if not self._state_path.exists():
37
+ return {}
38
+ try:
39
+ with self._state_path.open("r", encoding="utf-8") as fh:
40
+ data = json.load(fh)
41
+ if isinstance(data, dict):
42
+ return data
43
+ except Exception:
44
+ pass
45
+ return {}
46
+
47
+ def _save_state(self) -> None:
48
+ self._state_path.parent.mkdir(parents=True, exist_ok=True)
49
+ with self._state_path.open("w", encoding="utf-8") as fh:
50
+ json.dump(self._state, fh, indent=2, sort_keys=True)
51
+
52
+ def _ensure_entry(self) -> Dict[str, bool]:
53
+ return self._state.setdefault(self._package, {})
54
+
55
+ # --------------------------------------------------------------------- #
56
+ # Manual state management
57
+ # --------------------------------------------------------------------- #
58
+ def get_manual_state(self) -> Optional[bool]:
59
+ entry = self._state.get(self._package, {})
60
+ value = entry.get("manual")
61
+ return bool(value) if isinstance(value, bool) else None
62
+
63
+ def set_manual_state(self, value: Optional[bool]) -> None:
64
+ entry = self._ensure_entry()
65
+ if value is None:
66
+ entry.pop("manual", None)
67
+ else:
68
+ entry["manual"] = bool(value)
69
+ self._save_state()
70
+
71
+ # --------------------------------------------------------------------- #
72
+ # Auto detection cache
73
+ # --------------------------------------------------------------------- #
74
+ def get_cached_auto_state(self) -> Optional[bool]:
75
+ entry = self._state.get(self._package, {})
76
+ value = entry.get("auto")
77
+ return bool(value) if isinstance(value, bool) else None
78
+
79
+ def set_auto_state(self, value: Optional[bool]) -> None:
80
+ entry = self._ensure_entry()
81
+ if value is None:
82
+ entry.pop("auto", None)
83
+ else:
84
+ entry["auto"] = bool(value)
85
+ self._save_state()
86
+
@@ -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
+