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
exonware/__init__.py ADDED
@@ -0,0 +1,22 @@
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
+ Version: 0.1.0.19
8
+ Generation Date: 2025-01-03
9
+
10
+ This is a namespace package allowing multiple exonware subpackages
11
+ to coexist (xwsystem, xwnode, xwdata, xwlazy, etc.)
12
+ """
13
+
14
+ # Make this a namespace package - DO NOT set __path__
15
+ # This allows both exonware.xwsystem and exonware.xwlazy to coexist
16
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__)
17
+
18
+ __version__ = '0.0.1'
19
+ __author__ = 'Eng. Muhammad AlShehri'
20
+ __email__ = 'connect@exonware.com'
21
+ __company__ = 'eXonware.com'
22
+
Binary file
@@ -0,0 +1,47 @@
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
+ Version: 0.1.0.19
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
+ __all__ = [
32
+ # Logger
33
+ 'get_logger',
34
+ 'log_event',
35
+ 'print_formatted',
36
+ 'format_message',
37
+ 'is_log_category_enabled',
38
+ 'set_log_category',
39
+ 'set_log_categories',
40
+ 'get_log_categories',
41
+ 'XWLazyFormatter',
42
+ # Cache
43
+ 'MultiTierCache',
44
+ 'BytecodeCache',
45
+ 'InstallationCache',
46
+ ]
47
+
@@ -0,0 +1,58 @@
1
+ """
2
+ Common Abstract Base Classes
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
+ 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
16
+
17
+
18
+ # =============================================================================
19
+ # ABSTRACT CACHING STRATEGY
20
+ # =============================================================================
21
+
22
+ class ACachingStrategy(ICachingStrategy, ABC):
23
+ """
24
+ Abstract base class for caching strategies.
25
+
26
+ Works with ANY data type (modules, packages, etc.).
27
+ All caching strategies must extend this class.
28
+ """
29
+
30
+ @abstractmethod
31
+ def get(self, key: str) -> Optional[Any]:
32
+ """Get value from cache."""
33
+ ...
34
+
35
+ @abstractmethod
36
+ def set(self, key: str, value: Any) -> None:
37
+ """Set value in cache."""
38
+ ...
39
+
40
+ @abstractmethod
41
+ def invalidate(self, key: str) -> None:
42
+ """Invalidate cached value."""
43
+ ...
44
+
45
+ @abstractmethod
46
+ def clear(self) -> None:
47
+ """Clear all cached values."""
48
+ ...
49
+
50
+
51
+ # =============================================================================
52
+ # EXPORT ALL
53
+ # =============================================================================
54
+
55
+ __all__ = [
56
+ 'ACachingStrategy',
57
+ ]
58
+
@@ -0,0 +1,506 @@
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
+ Version: 0.1.0.19
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 Dict, List, Optional, Tuple, Any, Set
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
+ # =============================================================================
231
+ # BYTECODE CACHE
232
+ # =============================================================================
233
+
234
+ class BytecodeCache:
235
+ """
236
+ Bytecode caching for faster module loading.
237
+
238
+ Caches compiled Python bytecode to avoid recompilation on subsequent imports.
239
+ Used by module loading for performance optimization.
240
+ """
241
+
242
+ def __init__(self, cache_dir: Optional[Path] = None):
243
+ """
244
+ Initialize bytecode cache.
245
+
246
+ Args:
247
+ cache_dir: Directory for bytecode cache (defaults to ~/.xwlazy_bytecode)
248
+ """
249
+ self._cache_dir = cache_dir or Path.home() / ".xwlazy_bytecode"
250
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
251
+ self._lock = threading.RLock()
252
+
253
+ def get_bytecode_path(self, module_path: str) -> Path:
254
+ """
255
+ Get bytecode cache path for module.
256
+
257
+ Args:
258
+ module_path: Module path (e.g., "exonware.xwdata")
259
+
260
+ Returns:
261
+ Path to bytecode cache file
262
+ """
263
+ cache_name = f"{hash(module_path) % (2**31)}.pyc"
264
+ return self._cache_dir / cache_name
265
+
266
+ def get_cached_bytecode(self, module_path: str) -> Optional[bytes]:
267
+ """
268
+ Get cached bytecode if available and valid.
269
+
270
+ Args:
271
+ module_path: Module path
272
+
273
+ Returns:
274
+ Cached bytecode or None if not available/invalid
275
+ """
276
+ with self._lock:
277
+ cache_path = self.get_bytecode_path(module_path)
278
+ if not cache_path.exists():
279
+ return None
280
+
281
+ try:
282
+ # Check if source is newer than cache
283
+ source_path = self._find_source_path(module_path)
284
+ if source_path and source_path.exists():
285
+ source_mtime = source_path.stat().st_mtime
286
+ cache_mtime = cache_path.stat().st_mtime
287
+ if source_mtime > cache_mtime:
288
+ return None # Source is newer, cache invalid
289
+
290
+ # Read bytecode (skip 16-byte header)
291
+ with open(cache_path, 'rb') as f:
292
+ f.seek(16)
293
+ return f.read()
294
+ except Exception as e:
295
+ logger.debug(f"Failed to load bytecode cache for {module_path}: {e}")
296
+ return None
297
+
298
+ def cache_bytecode(self, module_path: str, bytecode: bytes) -> None:
299
+ """
300
+ Cache compiled bytecode.
301
+
302
+ Args:
303
+ module_path: Module path
304
+ bytecode: Compiled bytecode to cache
305
+ """
306
+ with self._lock:
307
+ try:
308
+ cache_path = self.get_bytecode_path(module_path)
309
+ with open(cache_path, 'wb') as f:
310
+ # Write Python bytecode header
311
+ f.write(importlib.util.MAGIC_NUMBER)
312
+ f.write(struct.pack('<I', int(time.time()))) # Timestamp
313
+ f.write(struct.pack('<I', 0)) # Size (0 = unknown)
314
+ f.write(bytecode)
315
+ except Exception as e:
316
+ logger.debug(f"Failed to cache bytecode for {module_path}: {e}")
317
+
318
+ def _find_source_path(self, module_path: str) -> Optional[Path]:
319
+ """
320
+ Find source file path for module.
321
+
322
+ Args:
323
+ module_path: Module path
324
+
325
+ Returns:
326
+ Path to source file or None if not found
327
+ """
328
+ try:
329
+ spec = importlib.util.find_spec(module_path)
330
+ if spec and spec.origin:
331
+ return Path(spec.origin)
332
+ except Exception:
333
+ pass
334
+ return None
335
+
336
+ def clear(self) -> None:
337
+ """Clear bytecode cache."""
338
+ with self._lock:
339
+ try:
340
+ for cache_file in self._cache_dir.glob("*.pyc"):
341
+ cache_file.unlink()
342
+ except Exception as e:
343
+ logger.debug(f"Failed to clear bytecode cache: {e}")
344
+
345
+
346
+ # =============================================================================
347
+ # INSTALLATION CACHE
348
+ # =============================================================================
349
+
350
+ class InstallationCache:
351
+ """
352
+ Persistent file-based cache for tracking installed packages.
353
+
354
+ Cache format: {package_name: {installed: bool, version: str, timestamp: float}}
355
+ Cache location: ~/.xwlazy/installed_packages.json
356
+
357
+ Used by package installer to track which packages are installed,
358
+ avoiding expensive importability checks on subsequent runs.
359
+ """
360
+
361
+ def __init__(self, cache_file: Optional[Path] = None):
362
+ """
363
+ Initialize installation cache.
364
+
365
+ Args:
366
+ cache_file: Optional path to cache file. Defaults to ~/.xwlazy/installed_packages.json
367
+ """
368
+ if cache_file is None:
369
+ cache_dir = Path.home() / ".xwlazy"
370
+ cache_dir.mkdir(parents=True, exist_ok=True)
371
+ cache_file = cache_dir / "installed_packages.json"
372
+
373
+ self._cache_file = cache_file
374
+ self._lock = threading.RLock()
375
+ self._cache: Dict[str, Dict[str, Any]] = {}
376
+ self._dirty = False
377
+
378
+ # Load cache on init
379
+ self._load_cache()
380
+
381
+ def _load_cache(self) -> None:
382
+ """Load cache from disk."""
383
+ if not self._cache_file.exists():
384
+ self._cache = {}
385
+ return
386
+
387
+ try:
388
+ with self._lock:
389
+ with open(self._cache_file, 'r', encoding='utf-8') as f:
390
+ data = json.load(f)
391
+ # Validate format
392
+ if isinstance(data, dict):
393
+ self._cache = {k: v for k, v in data.items()
394
+ if isinstance(v, dict) and 'installed' in v}
395
+ else:
396
+ self._cache = {}
397
+ except (json.JSONDecodeError, IOError, OSError) as e:
398
+ logger.debug(f"Failed to load installation cache: {e}")
399
+ self._cache = {}
400
+
401
+ def _save_cache(self) -> None:
402
+ """Save cache to disk."""
403
+ if not self._dirty:
404
+ return
405
+
406
+ try:
407
+ with self._lock:
408
+ # Create parent directory if needed
409
+ self._cache_file.parent.mkdir(parents=True, exist_ok=True)
410
+
411
+ # Write atomically using temp file
412
+ temp_file = self._cache_file.with_suffix('.tmp')
413
+ with open(temp_file, 'w', encoding='utf-8') as f:
414
+ json.dump(self._cache, f, indent=2, sort_keys=True)
415
+
416
+ # Atomic rename
417
+ temp_file.replace(self._cache_file)
418
+ self._dirty = False
419
+ except (IOError, OSError) as e:
420
+ logger.warning(f"Failed to save installation cache: {e}")
421
+
422
+ def is_installed(self, package_name: str) -> bool:
423
+ """
424
+ Check if package is marked as installed in cache.
425
+
426
+ Args:
427
+ package_name: Name of the package to check
428
+
429
+ Returns:
430
+ True if package is in cache and marked as installed, False otherwise
431
+ """
432
+ with self._lock:
433
+ entry = self._cache.get(package_name)
434
+ if entry is None:
435
+ return False
436
+ return entry.get('installed', False)
437
+
438
+ def mark_installed(self, package_name: str, version: Optional[str] = None) -> None:
439
+ """
440
+ Mark package as installed in cache.
441
+
442
+ Args:
443
+ package_name: Name of the package
444
+ version: Optional version string
445
+ """
446
+ with self._lock:
447
+ self._cache[package_name] = {
448
+ 'installed': True,
449
+ 'version': version or 'unknown',
450
+ 'timestamp': time.time()
451
+ }
452
+ self._dirty = True
453
+ self._save_cache()
454
+
455
+ def mark_uninstalled(self, package_name: str) -> None:
456
+ """
457
+ Mark package as uninstalled in cache.
458
+
459
+ Args:
460
+ package_name: Name of the package
461
+ """
462
+ with self._lock:
463
+ if package_name in self._cache:
464
+ self._cache[package_name]['installed'] = False
465
+ self._cache[package_name]['timestamp'] = time.time()
466
+ self._dirty = True
467
+ self._save_cache()
468
+
469
+ def get_version(self, package_name: str) -> Optional[str]:
470
+ """
471
+ Get cached version of package.
472
+
473
+ Args:
474
+ package_name: Name of the package
475
+
476
+ Returns:
477
+ Version string if available, None otherwise
478
+ """
479
+ with self._lock:
480
+ entry = self._cache.get(package_name)
481
+ if entry and entry.get('installed', False):
482
+ return entry.get('version')
483
+ return None
484
+
485
+ def clear(self) -> None:
486
+ """Clear all cache entries."""
487
+ with self._lock:
488
+ self._cache.clear()
489
+ self._dirty = True
490
+ self._save_cache()
491
+
492
+ def get_all_installed(self) -> Set[str]:
493
+ """
494
+ Get set of all packages marked as installed.
495
+
496
+ Returns:
497
+ Set of package names that are marked as installed
498
+ """
499
+ with self._lock:
500
+ return {name for name, entry in self._cache.items()
501
+ if entry.get('installed', False)}
502
+
503
+ def __len__(self) -> int:
504
+ """Return number of cached packages."""
505
+ return len(self._cache)
506
+