exonware-xwlazy 0.1.0.1__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 (93) hide show
  1. exonware/__init__.py +42 -0
  2. exonware/xwlazy/__init__.py +379 -0
  3. exonware/xwlazy/common/__init__.py +55 -0
  4. exonware/xwlazy/common/base.py +65 -0
  5. exonware/xwlazy/common/cache.py +504 -0
  6. exonware/xwlazy/common/logger.py +257 -0
  7. exonware/xwlazy/common/services/__init__.py +72 -0
  8. exonware/xwlazy/common/services/dependency_mapper.py +250 -0
  9. exonware/xwlazy/common/services/install_async_utils.py +170 -0
  10. exonware/xwlazy/common/services/install_cache_utils.py +245 -0
  11. exonware/xwlazy/common/services/keyword_detection.py +283 -0
  12. exonware/xwlazy/common/services/spec_cache.py +165 -0
  13. exonware/xwlazy/common/services/state_manager.py +84 -0
  14. exonware/xwlazy/common/strategies/__init__.py +28 -0
  15. exonware/xwlazy/common/strategies/caching_dict.py +44 -0
  16. exonware/xwlazy/common/strategies/caching_installation.py +88 -0
  17. exonware/xwlazy/common/strategies/caching_lfu.py +66 -0
  18. exonware/xwlazy/common/strategies/caching_lru.py +63 -0
  19. exonware/xwlazy/common/strategies/caching_multitier.py +59 -0
  20. exonware/xwlazy/common/strategies/caching_ttl.py +59 -0
  21. exonware/xwlazy/common/utils.py +142 -0
  22. exonware/xwlazy/config.py +193 -0
  23. exonware/xwlazy/contracts.py +1533 -0
  24. exonware/xwlazy/defs.py +378 -0
  25. exonware/xwlazy/errors.py +276 -0
  26. exonware/xwlazy/facade.py +1137 -0
  27. exonware/xwlazy/host/__init__.py +8 -0
  28. exonware/xwlazy/host/conf.py +16 -0
  29. exonware/xwlazy/module/__init__.py +18 -0
  30. exonware/xwlazy/module/base.py +643 -0
  31. exonware/xwlazy/module/data.py +17 -0
  32. exonware/xwlazy/module/facade.py +246 -0
  33. exonware/xwlazy/module/importer_engine.py +2964 -0
  34. exonware/xwlazy/module/partial_module_detector.py +275 -0
  35. exonware/xwlazy/module/strategies/__init__.py +22 -0
  36. exonware/xwlazy/module/strategies/module_helper_lazy.py +93 -0
  37. exonware/xwlazy/module/strategies/module_helper_simple.py +65 -0
  38. exonware/xwlazy/module/strategies/module_manager_advanced.py +111 -0
  39. exonware/xwlazy/module/strategies/module_manager_simple.py +95 -0
  40. exonware/xwlazy/package/__init__.py +18 -0
  41. exonware/xwlazy/package/base.py +877 -0
  42. exonware/xwlazy/package/conf.py +324 -0
  43. exonware/xwlazy/package/data.py +17 -0
  44. exonware/xwlazy/package/facade.py +480 -0
  45. exonware/xwlazy/package/services/__init__.py +84 -0
  46. exonware/xwlazy/package/services/async_install_handle.py +87 -0
  47. exonware/xwlazy/package/services/config_manager.py +249 -0
  48. exonware/xwlazy/package/services/discovery.py +435 -0
  49. exonware/xwlazy/package/services/host_packages.py +180 -0
  50. exonware/xwlazy/package/services/install_async.py +291 -0
  51. exonware/xwlazy/package/services/install_cache.py +145 -0
  52. exonware/xwlazy/package/services/install_interactive.py +59 -0
  53. exonware/xwlazy/package/services/install_policy.py +156 -0
  54. exonware/xwlazy/package/services/install_registry.py +54 -0
  55. exonware/xwlazy/package/services/install_result.py +17 -0
  56. exonware/xwlazy/package/services/install_sbom.py +153 -0
  57. exonware/xwlazy/package/services/install_utils.py +79 -0
  58. exonware/xwlazy/package/services/installer_engine.py +406 -0
  59. exonware/xwlazy/package/services/lazy_installer.py +803 -0
  60. exonware/xwlazy/package/services/manifest.py +503 -0
  61. exonware/xwlazy/package/services/strategy_registry.py +324 -0
  62. exonware/xwlazy/package/strategies/__init__.py +57 -0
  63. exonware/xwlazy/package/strategies/package_discovery_file.py +129 -0
  64. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +84 -0
  65. exonware/xwlazy/package/strategies/package_discovery_manifest.py +101 -0
  66. exonware/xwlazy/package/strategies/package_execution_async.py +113 -0
  67. exonware/xwlazy/package/strategies/package_execution_cached.py +90 -0
  68. exonware/xwlazy/package/strategies/package_execution_pip.py +99 -0
  69. exonware/xwlazy/package/strategies/package_execution_wheel.py +106 -0
  70. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +100 -0
  71. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +105 -0
  72. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +100 -0
  73. exonware/xwlazy/package/strategies/package_policy_allow_list.py +57 -0
  74. exonware/xwlazy/package/strategies/package_policy_deny_list.py +57 -0
  75. exonware/xwlazy/package/strategies/package_policy_permissive.py +46 -0
  76. exonware/xwlazy/package/strategies/package_timing_clean.py +67 -0
  77. exonware/xwlazy/package/strategies/package_timing_full.py +66 -0
  78. exonware/xwlazy/package/strategies/package_timing_smart.py +68 -0
  79. exonware/xwlazy/package/strategies/package_timing_temporary.py +66 -0
  80. exonware/xwlazy/runtime/__init__.py +18 -0
  81. exonware/xwlazy/runtime/adaptive_learner.py +129 -0
  82. exonware/xwlazy/runtime/base.py +274 -0
  83. exonware/xwlazy/runtime/facade.py +94 -0
  84. exonware/xwlazy/runtime/intelligent_selector.py +170 -0
  85. exonware/xwlazy/runtime/metrics.py +60 -0
  86. exonware/xwlazy/runtime/performance.py +37 -0
  87. exonware/xwlazy/version.py +77 -0
  88. exonware_xwlazy-0.1.0.1.dist-info/METADATA +454 -0
  89. exonware_xwlazy-0.1.0.1.dist-info/RECORD +93 -0
  90. exonware_xwlazy-0.1.0.1.dist-info/WHEEL +4 -0
  91. exonware_xwlazy-0.1.0.1.dist-info/licenses/LICENSE +21 -0
  92. xwlazy/__init__.py +14 -0
  93. xwlazy/lazy.py +30 -0
@@ -0,0 +1,504 @@
1
+ """
2
+ #exonware/xwlazy/src/exonware/xwlazy/common/cache.py
3
+
4
+ Cache utilities for xwlazy - shared across package, module, and runtime.
5
+
6
+ Company: eXonware.com
7
+ Author: Eng. Muhammad AlShehri
8
+ Email: connect@exonware.com
9
+
10
+ Generation Date: 15-Nov-2025
11
+
12
+ This module provides unified caching functionality for all xwlazy components.
13
+ All cache code is centralized here to avoid duplication.
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import json
19
+ import time
20
+ import pickle
21
+ import struct
22
+ import importlib
23
+ import importlib.util
24
+ import threading
25
+ from pathlib import Path
26
+ from typing import Optional, Any
27
+ from collections import OrderedDict
28
+ from queue import Queue
29
+
30
+ from .logger import get_logger
31
+
32
+ logger = get_logger("xwlazy.cache")
33
+
34
+ # =============================================================================
35
+ # MULTI-TIER CACHE
36
+ # =============================================================================
37
+
38
+ class MultiTierCache:
39
+ """
40
+ Multi-tier cache with L1 (memory), L2 (disk), L3 (predictive).
41
+
42
+ Used by package, module, and runtime for caching:
43
+ - Package installation status
44
+ - Module imports
45
+ - Runtime metrics and performance data
46
+ """
47
+
48
+ def __init__(self, l1_size: int = 1000, l2_dir: Optional[Path] = None, enable_l3: bool = True):
49
+ """
50
+ Initialize multi-tier cache.
51
+
52
+ Args:
53
+ l1_size: Maximum size of L1 (memory) cache
54
+ l2_dir: Directory for L2 (disk) cache (defaults to ~/.xwlazy_cache)
55
+ enable_l3: Enable L3 (predictive) cache
56
+ """
57
+ self._l1_cache: OrderedDict[str, Any] = OrderedDict()
58
+ self._l1_max_size = l1_size
59
+ self._l2_dir = l2_dir or Path.home() / ".xwlazy_cache"
60
+ self._l2_dir.mkdir(parents=True, exist_ok=True)
61
+ self._enable_l3 = enable_l3
62
+ self._l3_patterns: dict[str, tuple[int, float]] = {}
63
+ self._lock = threading.RLock()
64
+
65
+ self._l2_write_queue: Queue = Queue()
66
+ self._l2_write_thread: Optional[threading.Thread] = None
67
+ self._l2_write_stop = threading.Event()
68
+ self._start_l2_writer()
69
+
70
+ def get(self, key: str) -> Optional[Any]:
71
+ """
72
+ Get value from cache (L1 -> L2 -> L3).
73
+
74
+ Args:
75
+ key: Cache key
76
+
77
+ Returns:
78
+ Cached value or None if not found
79
+ """
80
+ # Check L1 (memory) cache first
81
+ if key in self._l1_cache:
82
+ with self._lock:
83
+ if key in self._l1_cache:
84
+ value = self._l1_cache.pop(key)
85
+ self._l1_cache[key] = value # Move to end (LRU)
86
+ self._update_l3_pattern(key)
87
+ return value
88
+
89
+ # Check L2 (disk) cache
90
+ l2_path = self._l2_dir / f"{hash(key) % (2**31)}.cache"
91
+ if l2_path.exists():
92
+ try:
93
+ with open(l2_path, 'rb') as f:
94
+ value = pickle.load(f)
95
+ with self._lock:
96
+ self._set_l1(key, value) # Promote to L1
97
+ self._update_l3_pattern(key)
98
+ return value
99
+ except Exception as e:
100
+ logger.debug(f"Failed to load L2 cache for {key}: {e}")
101
+
102
+ # L3 (predictive) - just logs, doesn't return
103
+ if self._enable_l3 and key in self._l3_patterns:
104
+ freq, _ = self._l3_patterns[key]
105
+ if freq > 5:
106
+ logger.debug(f"L3 pattern detected for {key} (freq: {freq})")
107
+
108
+ return None
109
+
110
+ def set(self, key: str, value: Any) -> None:
111
+ """
112
+ Set value in cache (L1 + L2 batched).
113
+
114
+ Args:
115
+ key: Cache key
116
+ value: Value to cache
117
+ """
118
+ with self._lock:
119
+ self._set_l1(key, value)
120
+ self._update_l3_pattern(key)
121
+
122
+ # Queue for L2 write (batched)
123
+ self._l2_write_queue.put((key, value))
124
+
125
+ def _set_l1(self, key: str, value: Any) -> None:
126
+ """Set value in L1 cache (internal, called with lock held)."""
127
+ if key in self._l1_cache:
128
+ self._l1_cache.move_to_end(key)
129
+ else:
130
+ self._l1_cache[key] = value
131
+ if len(self._l1_cache) > self._l1_max_size:
132
+ self._l1_cache.popitem(last=False) # Remove oldest (LRU)
133
+
134
+ def _set_l2(self, key: str, value: Any) -> None:
135
+ """Set value in L2 cache (internal, called by writer thread)."""
136
+ try:
137
+ l2_path = self._l2_dir / f"{hash(key) % (2**31)}.cache"
138
+ with open(l2_path, 'wb') as f:
139
+ pickle.dump(value, f)
140
+ except Exception as e:
141
+ logger.debug(f"Failed to save L2 cache for {key}: {e}")
142
+
143
+ def _start_l2_writer(self) -> None:
144
+ """Start background thread for batched L2 writes."""
145
+ def _l2_writer():
146
+ batch = []
147
+ batch_size = 10
148
+ batch_timeout = 0.1
149
+
150
+ while not self._l2_write_stop.is_set():
151
+ try:
152
+ try:
153
+ key, value = self._l2_write_queue.get(timeout=batch_timeout)
154
+ batch.append((key, value))
155
+
156
+ # Collect batch
157
+ for _ in range(batch_size - 1):
158
+ try:
159
+ key, value = self._l2_write_queue.get_nowait()
160
+ batch.append((key, value))
161
+ except:
162
+ break
163
+ except:
164
+ pass
165
+
166
+ # Write batch
167
+ if batch:
168
+ for key, value in batch:
169
+ self._set_l2(key, value)
170
+ batch.clear()
171
+ except Exception as e:
172
+ logger.debug(f"L2 writer error: {e}")
173
+
174
+ self._l2_write_thread = threading.Thread(target=_l2_writer, daemon=True, name="xwlazy-l2-writer")
175
+ self._l2_write_thread.start()
176
+
177
+ def shutdown(self) -> None:
178
+ """Shutdown L2 writer thread."""
179
+ self._l2_write_stop.set()
180
+ if self._l2_write_thread:
181
+ self._l2_write_thread.join(timeout=1.0)
182
+
183
+ def _update_l3_pattern(self, key: str) -> None:
184
+ """Update L3 access patterns (called with lock held)."""
185
+ if self._enable_l3:
186
+ freq, _ = self._l3_patterns.get(key, (0, 0.0))
187
+ self._l3_patterns[key] = (freq + 1, time.time())
188
+
189
+ # Prune old patterns if too many
190
+ if len(self._l3_patterns) > 10000:
191
+ sorted_patterns = sorted(self._l3_patterns.items(), key=lambda x: x[1][1])
192
+ for old_key, _ in sorted_patterns[:1000]:
193
+ del self._l3_patterns[old_key]
194
+
195
+ def get_predictive_keys(self, limit: int = 10) -> list[str]:
196
+ """
197
+ Get keys likely to be accessed soon (for preloading).
198
+
199
+ Args:
200
+ limit: Maximum number of keys to return
201
+
202
+ Returns:
203
+ List of keys sorted by access likelihood
204
+ """
205
+ with self._lock:
206
+ if not self._enable_l3:
207
+ return []
208
+
209
+ scored = [
210
+ (key, freq * (1.0 / (time.time() - last + 1.0)))
211
+ for key, (freq, last) in self._l3_patterns.items()
212
+ ]
213
+ scored.sort(key=lambda x: x[1], reverse=True)
214
+ return [key for key, _ in scored[:limit]]
215
+
216
+ def clear(self) -> None:
217
+ """Clear all cache tiers."""
218
+ with self._lock:
219
+ self._l1_cache.clear()
220
+ self._l3_patterns.clear()
221
+
222
+ # Clear L2 directory
223
+ try:
224
+ for cache_file in self._l2_dir.glob("*.cache"):
225
+ cache_file.unlink()
226
+ except Exception as e:
227
+ logger.debug(f"Failed to clear L2 cache: {e}")
228
+
229
+ # =============================================================================
230
+ # BYTECODE CACHE
231
+ # =============================================================================
232
+
233
+ class BytecodeCache:
234
+ """
235
+ Bytecode caching for faster module loading.
236
+
237
+ Caches compiled Python bytecode to avoid recompilation on subsequent imports.
238
+ Used by module loading for performance optimization.
239
+ """
240
+
241
+ def __init__(self, cache_dir: Optional[Path] = None):
242
+ """
243
+ Initialize bytecode cache.
244
+
245
+ Args:
246
+ cache_dir: Directory for bytecode cache (defaults to ~/.xwlazy_bytecode)
247
+ """
248
+ self._cache_dir = cache_dir or Path.home() / ".xwlazy_bytecode"
249
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
250
+ self._lock = threading.RLock()
251
+
252
+ def get_bytecode_path(self, module_path: str) -> Path:
253
+ """
254
+ Get bytecode cache path for module.
255
+
256
+ Args:
257
+ module_path: Module path (e.g., "exonware.xwdata")
258
+
259
+ Returns:
260
+ Path to bytecode cache file
261
+ """
262
+ cache_name = f"{hash(module_path) % (2**31)}.pyc"
263
+ return self._cache_dir / cache_name
264
+
265
+ def get_cached_bytecode(self, module_path: str) -> Optional[bytes]:
266
+ """
267
+ Get cached bytecode if available and valid.
268
+
269
+ Args:
270
+ module_path: Module path
271
+
272
+ Returns:
273
+ Cached bytecode or None if not available/invalid
274
+ """
275
+ with self._lock:
276
+ cache_path = self.get_bytecode_path(module_path)
277
+ if not cache_path.exists():
278
+ return None
279
+
280
+ try:
281
+ # Check if source is newer than cache
282
+ source_path = self._find_source_path(module_path)
283
+ if source_path and source_path.exists():
284
+ source_mtime = source_path.stat().st_mtime
285
+ cache_mtime = cache_path.stat().st_mtime
286
+ if source_mtime > cache_mtime:
287
+ return None # Source is newer, cache invalid
288
+
289
+ # Read bytecode (skip 16-byte header)
290
+ with open(cache_path, 'rb') as f:
291
+ f.seek(16)
292
+ return f.read()
293
+ except Exception as e:
294
+ logger.debug(f"Failed to load bytecode cache for {module_path}: {e}")
295
+ return None
296
+
297
+ def cache_bytecode(self, module_path: str, bytecode: bytes) -> None:
298
+ """
299
+ Cache compiled bytecode.
300
+
301
+ Args:
302
+ module_path: Module path
303
+ bytecode: Compiled bytecode to cache
304
+ """
305
+ with self._lock:
306
+ try:
307
+ cache_path = self.get_bytecode_path(module_path)
308
+ with open(cache_path, 'wb') as f:
309
+ # Write Python bytecode header
310
+ f.write(importlib.util.MAGIC_NUMBER)
311
+ f.write(struct.pack('<I', int(time.time()))) # Timestamp
312
+ f.write(struct.pack('<I', 0)) # Size (0 = unknown)
313
+ f.write(bytecode)
314
+ except Exception as e:
315
+ logger.debug(f"Failed to cache bytecode for {module_path}: {e}")
316
+
317
+ def _find_source_path(self, module_path: str) -> Optional[Path]:
318
+ """
319
+ Find source file path for module.
320
+
321
+ Args:
322
+ module_path: Module path
323
+
324
+ Returns:
325
+ Path to source file or None if not found
326
+ """
327
+ try:
328
+ spec = importlib.util.find_spec(module_path)
329
+ if spec and spec.origin:
330
+ return Path(spec.origin)
331
+ except Exception:
332
+ pass
333
+ return None
334
+
335
+ def clear(self) -> None:
336
+ """Clear bytecode cache."""
337
+ with self._lock:
338
+ try:
339
+ for cache_file in self._cache_dir.glob("*.pyc"):
340
+ cache_file.unlink()
341
+ except Exception as e:
342
+ logger.debug(f"Failed to clear bytecode cache: {e}")
343
+
344
+ # =============================================================================
345
+ # INSTALLATION CACHE
346
+ # =============================================================================
347
+
348
+ class InstallationCache:
349
+ """
350
+ Persistent file-based cache for tracking installed packages.
351
+
352
+ Cache format: {package_name: {installed: bool, version: str, timestamp: float}}
353
+ Cache location: ~/.xwlazy/installed_packages.json
354
+
355
+ Used by package installer to track which packages are installed,
356
+ avoiding expensive importability checks on subsequent runs.
357
+ """
358
+
359
+ def __init__(self, cache_file: Optional[Path] = None):
360
+ """
361
+ Initialize installation cache.
362
+
363
+ Args:
364
+ cache_file: Optional path to cache file. Defaults to ~/.xwlazy/installed_packages.json
365
+ """
366
+ if cache_file is None:
367
+ cache_dir = Path.home() / ".xwlazy"
368
+ cache_dir.mkdir(parents=True, exist_ok=True)
369
+ cache_file = cache_dir / "installed_packages.json"
370
+
371
+ self._cache_file = cache_file
372
+ self._lock = threading.RLock()
373
+ self._cache: dict[str, dict[str, Any]] = {}
374
+ self._dirty = False
375
+
376
+ # Load cache on init
377
+ self._load_cache()
378
+
379
+ def _load_cache(self) -> None:
380
+ """Load cache from disk."""
381
+ if not self._cache_file.exists():
382
+ self._cache = {}
383
+ return
384
+
385
+ try:
386
+ with self._lock:
387
+ with open(self._cache_file, 'r', encoding='utf-8') as f:
388
+ data = json.load(f)
389
+ # Validate format
390
+ if isinstance(data, dict):
391
+ self._cache = {k: v for k, v in data.items()
392
+ if isinstance(v, dict) and 'installed' in v}
393
+ else:
394
+ self._cache = {}
395
+ except (json.JSONDecodeError, IOError, OSError) as e:
396
+ logger.debug(f"Failed to load installation cache: {e}")
397
+ self._cache = {}
398
+
399
+ def _save_cache(self) -> None:
400
+ """Save cache to disk."""
401
+ if not self._dirty:
402
+ return
403
+
404
+ try:
405
+ with self._lock:
406
+ # Create parent directory if needed
407
+ self._cache_file.parent.mkdir(parents=True, exist_ok=True)
408
+
409
+ # Write atomically using temp file
410
+ temp_file = self._cache_file.with_suffix('.tmp')
411
+ with open(temp_file, 'w', encoding='utf-8') as f:
412
+ json.dump(self._cache, f, indent=2, sort_keys=True)
413
+
414
+ # Atomic rename
415
+ temp_file.replace(self._cache_file)
416
+ self._dirty = False
417
+ except (IOError, OSError) as e:
418
+ logger.warning(f"Failed to save installation cache: {e}")
419
+
420
+ def is_installed(self, package_name: str) -> bool:
421
+ """
422
+ Check if package is marked as installed in cache.
423
+
424
+ Args:
425
+ package_name: Name of the package to check
426
+
427
+ Returns:
428
+ True if package is in cache and marked as installed, False otherwise
429
+ """
430
+ with self._lock:
431
+ entry = self._cache.get(package_name)
432
+ if entry is None:
433
+ return False
434
+ return entry.get('installed', False)
435
+
436
+ def mark_installed(self, package_name: str, version: Optional[str] = None) -> None:
437
+ """
438
+ Mark package as installed in cache.
439
+
440
+ Args:
441
+ package_name: Name of the package
442
+ version: Optional version string
443
+ """
444
+ with self._lock:
445
+ self._cache[package_name] = {
446
+ 'installed': True,
447
+ 'version': version or 'unknown',
448
+ 'timestamp': time.time()
449
+ }
450
+ self._dirty = True
451
+ self._save_cache()
452
+
453
+ def mark_uninstalled(self, package_name: str) -> None:
454
+ """
455
+ Mark package as uninstalled in cache.
456
+
457
+ Args:
458
+ package_name: Name of the package
459
+ """
460
+ with self._lock:
461
+ if package_name in self._cache:
462
+ self._cache[package_name]['installed'] = False
463
+ self._cache[package_name]['timestamp'] = time.time()
464
+ self._dirty = True
465
+ self._save_cache()
466
+
467
+ def get_version(self, package_name: str) -> Optional[str]:
468
+ """
469
+ Get cached version of package.
470
+
471
+ Args:
472
+ package_name: Name of the package
473
+
474
+ Returns:
475
+ Version string if available, None otherwise
476
+ """
477
+ with self._lock:
478
+ entry = self._cache.get(package_name)
479
+ if entry and entry.get('installed', False):
480
+ return entry.get('version')
481
+ return None
482
+
483
+ def clear(self) -> None:
484
+ """Clear all cache entries."""
485
+ with self._lock:
486
+ self._cache.clear()
487
+ self._dirty = True
488
+ self._save_cache()
489
+
490
+ def get_all_installed(self) -> set[str]:
491
+ """
492
+ Get set of all packages marked as installed.
493
+
494
+ Returns:
495
+ Set of package names that are marked as installed
496
+ """
497
+ with self._lock:
498
+ return {name for name, entry in self._cache.items()
499
+ if entry.get('installed', False)}
500
+
501
+ def __len__(self) -> int:
502
+ """Return number of cached packages."""
503
+ return len(self._cache)
504
+