exonware-xwlazy 0.1.0.1__py3-none-any.whl → 0.1.0.9__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 (95) hide show
  1. exonware/xwlazy/__init__.py +0 -0
  2. exonware/xwlazy/version.py +2 -2
  3. exonware_xwlazy-0.1.0.9.dist-info/METADATA +0 -0
  4. exonware_xwlazy-0.1.0.9.dist-info/RECORD +6 -0
  5. exonware/__init__.py +0 -42
  6. exonware/xwlazy/common/__init__.py +0 -55
  7. exonware/xwlazy/common/base.py +0 -65
  8. exonware/xwlazy/common/cache.py +0 -504
  9. exonware/xwlazy/common/logger.py +0 -257
  10. exonware/xwlazy/common/services/__init__.py +0 -72
  11. exonware/xwlazy/common/services/dependency_mapper.py +0 -250
  12. exonware/xwlazy/common/services/install_async_utils.py +0 -170
  13. exonware/xwlazy/common/services/install_cache_utils.py +0 -245
  14. exonware/xwlazy/common/services/keyword_detection.py +0 -283
  15. exonware/xwlazy/common/services/spec_cache.py +0 -165
  16. exonware/xwlazy/common/services/state_manager.py +0 -84
  17. exonware/xwlazy/common/strategies/__init__.py +0 -28
  18. exonware/xwlazy/common/strategies/caching_dict.py +0 -44
  19. exonware/xwlazy/common/strategies/caching_installation.py +0 -88
  20. exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
  21. exonware/xwlazy/common/strategies/caching_lru.py +0 -63
  22. exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
  23. exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
  24. exonware/xwlazy/common/utils.py +0 -142
  25. exonware/xwlazy/config.py +0 -193
  26. exonware/xwlazy/contracts.py +0 -1533
  27. exonware/xwlazy/defs.py +0 -378
  28. exonware/xwlazy/errors.py +0 -276
  29. exonware/xwlazy/facade.py +0 -1137
  30. exonware/xwlazy/host/__init__.py +0 -8
  31. exonware/xwlazy/host/conf.py +0 -16
  32. exonware/xwlazy/module/__init__.py +0 -18
  33. exonware/xwlazy/module/base.py +0 -643
  34. exonware/xwlazy/module/data.py +0 -17
  35. exonware/xwlazy/module/facade.py +0 -246
  36. exonware/xwlazy/module/importer_engine.py +0 -2964
  37. exonware/xwlazy/module/partial_module_detector.py +0 -275
  38. exonware/xwlazy/module/strategies/__init__.py +0 -22
  39. exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
  40. exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
  41. exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
  42. exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
  43. exonware/xwlazy/package/__init__.py +0 -18
  44. exonware/xwlazy/package/base.py +0 -877
  45. exonware/xwlazy/package/conf.py +0 -324
  46. exonware/xwlazy/package/data.py +0 -17
  47. exonware/xwlazy/package/facade.py +0 -480
  48. exonware/xwlazy/package/services/__init__.py +0 -84
  49. exonware/xwlazy/package/services/async_install_handle.py +0 -87
  50. exonware/xwlazy/package/services/config_manager.py +0 -249
  51. exonware/xwlazy/package/services/discovery.py +0 -435
  52. exonware/xwlazy/package/services/host_packages.py +0 -180
  53. exonware/xwlazy/package/services/install_async.py +0 -291
  54. exonware/xwlazy/package/services/install_cache.py +0 -145
  55. exonware/xwlazy/package/services/install_interactive.py +0 -59
  56. exonware/xwlazy/package/services/install_policy.py +0 -156
  57. exonware/xwlazy/package/services/install_registry.py +0 -54
  58. exonware/xwlazy/package/services/install_result.py +0 -17
  59. exonware/xwlazy/package/services/install_sbom.py +0 -153
  60. exonware/xwlazy/package/services/install_utils.py +0 -79
  61. exonware/xwlazy/package/services/installer_engine.py +0 -406
  62. exonware/xwlazy/package/services/lazy_installer.py +0 -803
  63. exonware/xwlazy/package/services/manifest.py +0 -503
  64. exonware/xwlazy/package/services/strategy_registry.py +0 -324
  65. exonware/xwlazy/package/strategies/__init__.py +0 -57
  66. exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
  67. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
  68. exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
  69. exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
  70. exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
  71. exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
  72. exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
  73. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
  74. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
  75. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
  76. exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
  77. exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
  78. exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
  79. exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
  80. exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
  81. exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
  82. exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
  83. exonware/xwlazy/runtime/__init__.py +0 -18
  84. exonware/xwlazy/runtime/adaptive_learner.py +0 -129
  85. exonware/xwlazy/runtime/base.py +0 -274
  86. exonware/xwlazy/runtime/facade.py +0 -94
  87. exonware/xwlazy/runtime/intelligent_selector.py +0 -170
  88. exonware/xwlazy/runtime/metrics.py +0 -60
  89. exonware/xwlazy/runtime/performance.py +0 -37
  90. exonware_xwlazy-0.1.0.1.dist-info/METADATA +0 -454
  91. exonware_xwlazy-0.1.0.1.dist-info/RECORD +0 -93
  92. xwlazy/__init__.py +0 -14
  93. xwlazy/lazy.py +0 -30
  94. {exonware_xwlazy-0.1.0.1.dist-info → exonware_xwlazy-0.1.0.9.dist-info}/WHEEL +0 -0
  95. {exonware_xwlazy-0.1.0.1.dist-info → exonware_xwlazy-0.1.0.9.dist-info}/licenses/LICENSE +0 -0
Binary file
@@ -14,13 +14,13 @@ All version references should import from this module to ensure consistency.
14
14
  # =============================================================================
15
15
 
16
16
  # Main version - update this to change version across entire project
17
- __version__ = "0.1.0.1"
17
+ __version__ = "0.1.0.9"
18
18
 
19
19
  # Version components for programmatic access
20
20
  VERSION_MAJOR = 0
21
21
  VERSION_MINOR = 1
22
22
  VERSION_PATCH = 0
23
- VERSION_BUILD = 1 # Set to None for releases, or build number for dev builds
23
+ VERSION_BUILD = 9 # Set to None for releases, or build number for dev builds
24
24
 
25
25
  # Version metadata
26
26
  VERSION_SUFFIX = "" # e.g., "dev", "alpha", "beta", "rc1"
@@ -0,0 +1,6 @@
1
+ exonware/xwlazy/__init__.py,sha256=ZXpGiNGjj5KRB6q4nDas863XgPcfOa89X8YTC2M_W2o,448
2
+ exonware/xwlazy/version.py,sha256=5NutoLHVDnPmWSl88r-na_F7RH1HOpJgwEyavYK4fBg,2349
3
+ exonware_xwlazy-0.1.0.9.dist-info/METADATA,sha256=54aU__J_zsmCyNPDU56kcI2Hw3suY1nU3Los3C6cluM,1098
4
+ exonware_xwlazy-0.1.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ exonware_xwlazy-0.1.0.9.dist-info/licenses/LICENSE,sha256=w42ohoEUfhyT0NgiivAL4fWg2AMRLGnfXPMAR4EO-MU,1094
6
+ exonware_xwlazy-0.1.0.9.dist-info/RECORD,,
exonware/__init__.py DELETED
@@ -1,42 +0,0 @@
1
- """
2
- exonware package - Enterprise-grade Python framework ecosystem
3
-
4
- Company: eXonware.com
5
- Author: Eng. Muhammad AlShehri
6
- Email: connect@exonware.com
7
- Generation Date: 2025-01-03
8
-
9
- This is a namespace package allowing multiple exonware subpackages
10
- to coexist (xwsystem, xwnode, xwdata, xwlazy, etc.)
11
- """
12
-
13
- # Make this a namespace package FIRST
14
- # This allows both exonware.xwsystem and exonware.xwlazy to coexist
15
- __path__ = __import__('pkgutil').extend_path(__path__, __name__)
16
-
17
- # Import version from xwlazy - required, no fallback
18
- from exonware.xwlazy.version import __version__
19
-
20
- __author__ = 'Eng. Muhammad AlShehri'
21
- __email__ = 'connect@exonware.com'
22
- __company__ = 'eXonware.com'
23
-
24
- # NOW enable lazy mode (after namespace package is set up)
25
- import sys
26
- import importlib
27
- try:
28
- # Use importlib to import after namespace is ready
29
- if 'exonware.xwlazy' not in sys.modules:
30
- xwlazy_module = importlib.import_module('exonware.xwlazy')
31
- auto_enable_lazy = getattr(xwlazy_module, 'auto_enable_lazy', None)
32
- if auto_enable_lazy:
33
- auto_enable_lazy("xwsystem", mode="smart")
34
- print("✅ Lazy mode enabled for xwsystem")
35
- else:
36
- # Module already loaded, use it directly
37
- from exonware.xwlazy import auto_enable_lazy
38
- auto_enable_lazy("xwsystem", mode="smart")
39
- print("✅ Lazy mode enabled for xwsystem")
40
- except (ImportError, AttributeError):
41
- print("❌ Lazy mode not enabled for xwsystem (xwlazy not installed)")
42
- pass # xwlazy not installed - silently continue
@@ -1,55 +0,0 @@
1
- """
2
- #exonware/xwlazy/src/exonware/xwlazy/common/__init__.py
3
-
4
- Common utilities 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
-
13
- from .logger import (
14
- get_logger,
15
- log_event,
16
- print_formatted,
17
- format_message,
18
- is_log_category_enabled,
19
- set_log_category,
20
- set_log_categories,
21
- get_log_categories,
22
- XWLazyFormatter,
23
- )
24
-
25
- from .cache import (
26
- MultiTierCache,
27
- BytecodeCache,
28
- InstallationCache,
29
- )
30
-
31
- from .utils import (
32
- find_project_root,
33
- find_config_file,
34
- )
35
-
36
- __all__ = [
37
- # Logger
38
- 'get_logger',
39
- 'log_event',
40
- 'print_formatted',
41
- 'format_message',
42
- 'is_log_category_enabled',
43
- 'set_log_category',
44
- 'set_log_categories',
45
- 'get_log_categories',
46
- 'XWLazyFormatter',
47
- # Cache
48
- 'MultiTierCache',
49
- 'BytecodeCache',
50
- 'InstallationCache',
51
- # Utils
52
- 'find_project_root',
53
- 'find_config_file',
54
- ]
55
-
@@ -1,65 +0,0 @@
1
- """
2
- Common Abstract Base Classes
3
-
4
- Company: eXonware.com
5
- Author: Eng. Muhammad AlShehri
6
- Email: connect@exonware.com
7
-
8
- Generation Date: 15-Nov-2025
9
-
10
- Abstract base classes for shared/common strategies.
11
- """
12
-
13
- from abc import ABC, abstractmethod
14
- from typing import Optional, Any
15
- from ..contracts import ICachingStrategy, ICacheStrategy
16
-
17
- # =============================================================================
18
- # ABSTRACT CACHING STRATEGY
19
- # =============================================================================
20
-
21
- class ACachingStrategy(ICachingStrategy, ABC):
22
- """
23
- Abstract base class for caching strategies (legacy name).
24
-
25
- Note: Use ACacheStrategy for new code (ICacheStrategy interface).
26
- """
27
- pass
28
-
29
- class ACacheStrategy(ICacheStrategy, ABC):
30
- """
31
- Abstract base class for caching strategies.
32
-
33
- Works with ANY data type (modules, packages, etc.).
34
- All caching strategies must extend this class.
35
- """
36
-
37
- @abstractmethod
38
- def get(self, key: str) -> Optional[Any]:
39
- """Get value from cache."""
40
- ...
41
-
42
- @abstractmethod
43
- def set(self, key: str, value: Any) -> None:
44
- """Set value in cache."""
45
- ...
46
-
47
- @abstractmethod
48
- def invalidate(self, key: str) -> None:
49
- """Invalidate cached value."""
50
- ...
51
-
52
- @abstractmethod
53
- def clear(self) -> None:
54
- """Clear all cached values."""
55
- ...
56
-
57
- # =============================================================================
58
- # EXPORT ALL
59
- # =============================================================================
60
-
61
- __all__ = [
62
- 'ACachingStrategy', # Legacy name
63
- 'ACacheStrategy', # New name for ICacheStrategy interface
64
- ]
65
-
@@ -1,504 +0,0 @@
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
-