exonware-xwlazy 0.1.0.10__py3-none-any.whl → 0.1.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exonware/xwlazy/__init__.py +0 -0
- exonware/xwlazy/version.py +2 -2
- exonware_xwlazy-0.1.0.11.dist-info/METADATA +380 -0
- exonware_xwlazy-0.1.0.11.dist-info/RECORD +20 -0
- xwlazy/__init__.py +34 -0
- xwlazy/lazy/__init__.py +301 -0
- xwlazy/lazy/bootstrap.py +106 -0
- xwlazy/lazy/config.py +163 -0
- xwlazy/lazy/host_conf.py +279 -0
- xwlazy/lazy/host_packages.py +122 -0
- xwlazy/lazy/lazy_base.py +465 -0
- xwlazy/lazy/lazy_contracts.py +290 -0
- xwlazy/lazy/lazy_core.py +3727 -0
- xwlazy/lazy/lazy_errors.py +271 -0
- xwlazy/lazy/lazy_state.py +86 -0
- xwlazy/lazy/logging_utils.py +194 -0
- xwlazy/lazy/manifest.py +489 -0
- xwlazy/version.py +77 -0
- exonware_xwlazy-0.1.0.10.dist-info/METADATA +0 -0
- exonware_xwlazy-0.1.0.10.dist-info/RECORD +0 -6
- {exonware_xwlazy-0.1.0.10.dist-info → exonware_xwlazy-0.1.0.11.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.10.dist-info → exonware_xwlazy-0.1.0.11.dist-info}/licenses/LICENSE +0 -0
xwlazy/lazy/lazy_base.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""
|
|
2
|
+
#exonware/xwsystem/src/exonware/xwsystem/utils/lazy_package/lazy_base.py
|
|
3
|
+
|
|
4
|
+
Company: eXonware.com
|
|
5
|
+
Author: Eng. Muhammad AlShehri
|
|
6
|
+
Email: connect@exonware.com
|
|
7
|
+
Version: 0.1.0.16
|
|
8
|
+
Generation Date: 10-Oct-2025
|
|
9
|
+
|
|
10
|
+
Abstract Base Classes for Lazy Loading System
|
|
11
|
+
|
|
12
|
+
This module defines all abstract base classes for the lazy loading system
|
|
13
|
+
following DEV_GUIDELINES.md structure. All abstract classes start with 'A'
|
|
14
|
+
and extend interfaces from lazy_contracts.py.
|
|
15
|
+
|
|
16
|
+
Design Patterns:
|
|
17
|
+
- Template Method: Base classes define common workflows with abstract steps
|
|
18
|
+
- Strategy: Different implementations can be plugged in
|
|
19
|
+
- Abstract Factory: Factory methods for creating instances
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import threading
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Dict, List, Optional, Any, Set, Tuple
|
|
26
|
+
from types import ModuleType
|
|
27
|
+
|
|
28
|
+
from .lazy_contracts import (
|
|
29
|
+
IPackageDiscovery,
|
|
30
|
+
IPackageInstaller,
|
|
31
|
+
IImportHook,
|
|
32
|
+
IPackageCache,
|
|
33
|
+
ILazyLoader,
|
|
34
|
+
DependencyInfo,
|
|
35
|
+
LazyInstallMode,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# ABSTRACT DISCOVERY (Template Method Pattern)
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
class APackageDiscovery(IPackageDiscovery, ABC):
|
|
44
|
+
"""
|
|
45
|
+
Abstract base for package discovery.
|
|
46
|
+
|
|
47
|
+
Implements Template Method pattern where discover_all_dependencies()
|
|
48
|
+
defines the overall workflow, and subclasses implement specific steps.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
__slots__ = ('project_root', 'discovered_dependencies', '_discovery_sources',
|
|
52
|
+
'_cached_dependencies', '_file_mtimes', '_cache_valid')
|
|
53
|
+
|
|
54
|
+
def __init__(self, project_root: Optional[str] = None):
|
|
55
|
+
"""
|
|
56
|
+
Initialize package discovery.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
project_root: Root directory of project (auto-detected if None)
|
|
60
|
+
"""
|
|
61
|
+
self.project_root = Path(project_root) if project_root else self._find_project_root()
|
|
62
|
+
self.discovered_dependencies: Dict[str, DependencyInfo] = {}
|
|
63
|
+
self._discovery_sources: List[str] = []
|
|
64
|
+
self._cached_dependencies: Dict[str, str] = {}
|
|
65
|
+
self._file_mtimes: Dict[str, float] = {}
|
|
66
|
+
self._cache_valid = False
|
|
67
|
+
|
|
68
|
+
def _find_project_root(self) -> Path:
|
|
69
|
+
"""Find the project root directory by looking for markers."""
|
|
70
|
+
current = Path(__file__).parent
|
|
71
|
+
while current != current.parent:
|
|
72
|
+
if (current / 'pyproject.toml').exists() or (current / 'setup.py').exists():
|
|
73
|
+
return current
|
|
74
|
+
current = current.parent
|
|
75
|
+
return Path.cwd()
|
|
76
|
+
|
|
77
|
+
def discover_all_dependencies(self) -> Dict[str, str]:
|
|
78
|
+
"""
|
|
79
|
+
Template method: Discover all dependencies from all sources.
|
|
80
|
+
|
|
81
|
+
Workflow:
|
|
82
|
+
1. Check if cache is valid
|
|
83
|
+
2. If not, discover from sources
|
|
84
|
+
3. Add common mappings
|
|
85
|
+
4. Update cache
|
|
86
|
+
5. Return dependencies
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dict mapping import_name -> package_name
|
|
90
|
+
"""
|
|
91
|
+
# Return cached result if still valid
|
|
92
|
+
if self._is_cache_valid():
|
|
93
|
+
return self._cached_dependencies.copy()
|
|
94
|
+
|
|
95
|
+
# Cache invalid - rediscover
|
|
96
|
+
self.discovered_dependencies.clear()
|
|
97
|
+
self._discovery_sources.clear()
|
|
98
|
+
|
|
99
|
+
# Discover from all sources (abstract method)
|
|
100
|
+
self._discover_from_sources()
|
|
101
|
+
|
|
102
|
+
# Add common mappings
|
|
103
|
+
self._add_common_mappings()
|
|
104
|
+
|
|
105
|
+
# Convert to simple dict format and cache
|
|
106
|
+
result = {}
|
|
107
|
+
for import_name, dep_info in self.discovered_dependencies.items():
|
|
108
|
+
result[import_name] = dep_info.package_name
|
|
109
|
+
|
|
110
|
+
# Update cache
|
|
111
|
+
self._cached_dependencies = result.copy()
|
|
112
|
+
self._cache_valid = True
|
|
113
|
+
self._update_file_mtimes()
|
|
114
|
+
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def _discover_from_sources(self) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Discover dependencies from all sources (abstract step).
|
|
121
|
+
|
|
122
|
+
Implementations should discover from:
|
|
123
|
+
- pyproject.toml
|
|
124
|
+
- requirements.txt
|
|
125
|
+
- setup.py
|
|
126
|
+
- custom config files
|
|
127
|
+
"""
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def _is_cache_valid(self) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Check if cached dependencies are still valid (abstract step).
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if cache is valid, False otherwise
|
|
137
|
+
"""
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def _add_common_mappings(self) -> None:
|
|
142
|
+
"""Add common import -> package mappings (abstract step)."""
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
def _update_file_mtimes(self) -> None:
|
|
147
|
+
"""Update file modification times for cache validation (abstract step)."""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def get_discovery_sources(self) -> List[str]:
|
|
151
|
+
"""Get list of sources used for discovery."""
|
|
152
|
+
return self._discovery_sources.copy()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# =============================================================================
|
|
156
|
+
# ABSTRACT INSTALLER (Strategy Pattern)
|
|
157
|
+
# =============================================================================
|
|
158
|
+
|
|
159
|
+
class APackageInstaller(IPackageInstaller, ABC):
|
|
160
|
+
"""
|
|
161
|
+
Abstract base for package installation.
|
|
162
|
+
|
|
163
|
+
Implements Strategy pattern for different installation modes:
|
|
164
|
+
- AUTO: Automatically install without asking
|
|
165
|
+
- INTERACTIVE: Ask user before installing
|
|
166
|
+
- WARN: Log warning but don't install
|
|
167
|
+
- DISABLED: Don't install anything
|
|
168
|
+
- DRY_RUN: Show what would be installed but don't install
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
__slots__ = ('_package_name', '_enabled', '_mode', '_installed_packages',
|
|
172
|
+
'_failed_packages', '_lock')
|
|
173
|
+
|
|
174
|
+
def __init__(self, package_name: str = 'default'):
|
|
175
|
+
"""
|
|
176
|
+
Initialize package installer.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
package_name: Name of package this installer is for (for isolation)
|
|
180
|
+
"""
|
|
181
|
+
self._package_name = package_name
|
|
182
|
+
self._enabled = False
|
|
183
|
+
self._mode = LazyInstallMode.AUTO
|
|
184
|
+
self._installed_packages: Set[str] = set()
|
|
185
|
+
self._failed_packages: Set[str] = set()
|
|
186
|
+
self._lock = threading.RLock()
|
|
187
|
+
|
|
188
|
+
def get_package_name(self) -> str:
|
|
189
|
+
"""Get the package name this installer is for."""
|
|
190
|
+
return self._package_name
|
|
191
|
+
|
|
192
|
+
def set_mode(self, mode: LazyInstallMode) -> None:
|
|
193
|
+
"""Set the installation mode."""
|
|
194
|
+
with self._lock:
|
|
195
|
+
self._mode = mode
|
|
196
|
+
|
|
197
|
+
def get_mode(self) -> LazyInstallMode:
|
|
198
|
+
"""Get the current installation mode."""
|
|
199
|
+
return self._mode
|
|
200
|
+
|
|
201
|
+
def enable(self) -> None:
|
|
202
|
+
"""Enable lazy installation."""
|
|
203
|
+
with self._lock:
|
|
204
|
+
self._enabled = True
|
|
205
|
+
|
|
206
|
+
def disable(self) -> None:
|
|
207
|
+
"""Disable lazy installation."""
|
|
208
|
+
with self._lock:
|
|
209
|
+
self._enabled = False
|
|
210
|
+
|
|
211
|
+
def is_enabled(self) -> bool:
|
|
212
|
+
"""Check if lazy installation is enabled."""
|
|
213
|
+
return self._enabled
|
|
214
|
+
|
|
215
|
+
@abstractmethod
|
|
216
|
+
def install_package(self, package_name: str, module_name: str = None) -> bool:
|
|
217
|
+
"""
|
|
218
|
+
Install a package (abstract method).
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
package_name: Name of package to install
|
|
222
|
+
module_name: Name of module being imported (for interactive mode)
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if installation successful, False otherwise
|
|
226
|
+
"""
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
@abstractmethod
|
|
230
|
+
def _check_security_policy(self, package_name: str) -> Tuple[bool, str]:
|
|
231
|
+
"""
|
|
232
|
+
Check security policy for package (abstract method).
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
package_name: Package to check
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Tuple of (allowed: bool, reason: str)
|
|
239
|
+
"""
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
@abstractmethod
|
|
243
|
+
def _run_pip_install(self, package_name: str, args: List[str]) -> bool:
|
|
244
|
+
"""
|
|
245
|
+
Run pip install with arguments (abstract method).
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
package_name: Package to install
|
|
249
|
+
args: Additional pip arguments
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if successful, False otherwise
|
|
253
|
+
"""
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
257
|
+
"""Get installation statistics."""
|
|
258
|
+
with self._lock:
|
|
259
|
+
return {
|
|
260
|
+
'enabled': self._enabled,
|
|
261
|
+
'mode': self._mode.value,
|
|
262
|
+
'package_name': self._package_name,
|
|
263
|
+
'installed_packages': list(self._installed_packages),
|
|
264
|
+
'failed_packages': list(self._failed_packages),
|
|
265
|
+
'total_installed': len(self._installed_packages),
|
|
266
|
+
'total_failed': len(self._failed_packages)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# =============================================================================
|
|
271
|
+
# ABSTRACT IMPORT HOOK (Observer Pattern)
|
|
272
|
+
# =============================================================================
|
|
273
|
+
|
|
274
|
+
class AImportHook(IImportHook, ABC):
|
|
275
|
+
"""
|
|
276
|
+
Abstract base for import hooks.
|
|
277
|
+
|
|
278
|
+
Implements Observer pattern to observe import failures and trigger
|
|
279
|
+
lazy installation when needed.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
__slots__ = ('_package_name', '_enabled')
|
|
283
|
+
|
|
284
|
+
def __init__(self, package_name: str = 'default'):
|
|
285
|
+
"""
|
|
286
|
+
Initialize import hook.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
package_name: Package this hook is for
|
|
290
|
+
"""
|
|
291
|
+
self._package_name = package_name
|
|
292
|
+
self._enabled = True
|
|
293
|
+
|
|
294
|
+
def enable(self) -> None:
|
|
295
|
+
"""Enable the import hook."""
|
|
296
|
+
self._enabled = True
|
|
297
|
+
|
|
298
|
+
def disable(self) -> None:
|
|
299
|
+
"""Disable the import hook."""
|
|
300
|
+
self._enabled = False
|
|
301
|
+
|
|
302
|
+
def is_enabled(self) -> bool:
|
|
303
|
+
"""Check if hook is enabled."""
|
|
304
|
+
return self._enabled
|
|
305
|
+
|
|
306
|
+
@abstractmethod
|
|
307
|
+
def install_hook(self) -> None:
|
|
308
|
+
"""Install the import hook into sys.meta_path (abstract method)."""
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
@abstractmethod
|
|
312
|
+
def uninstall_hook(self) -> None:
|
|
313
|
+
"""Uninstall the import hook from sys.meta_path (abstract method)."""
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
@abstractmethod
|
|
317
|
+
def handle_import_error(self, module_name: str) -> Optional[Any]:
|
|
318
|
+
"""
|
|
319
|
+
Handle ImportError by attempting to install and re-import (abstract method).
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
module_name: Name of module that failed to import
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Imported module if successful, None otherwise
|
|
326
|
+
"""
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# =============================================================================
|
|
331
|
+
# ABSTRACT CACHE (Proxy Pattern)
|
|
332
|
+
# =============================================================================
|
|
333
|
+
|
|
334
|
+
class APackageCache(IPackageCache, ABC):
|
|
335
|
+
"""
|
|
336
|
+
Abstract base for package caching.
|
|
337
|
+
|
|
338
|
+
Implements Proxy pattern to provide cached access to packages
|
|
339
|
+
and avoid repeated operations.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
__slots__ = ('_cache', '_lock')
|
|
343
|
+
|
|
344
|
+
def __init__(self):
|
|
345
|
+
"""Initialize package cache."""
|
|
346
|
+
self._cache: Dict[str, Any] = {}
|
|
347
|
+
self._lock = threading.RLock()
|
|
348
|
+
|
|
349
|
+
@abstractmethod
|
|
350
|
+
def get_cached(self, key: str) -> Optional[Any]:
|
|
351
|
+
"""
|
|
352
|
+
Get cached value (abstract method).
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
key: Cache key
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Cached value or None if not found
|
|
359
|
+
"""
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
@abstractmethod
|
|
363
|
+
def set_cached(self, key: str, value: Any) -> None:
|
|
364
|
+
"""
|
|
365
|
+
Set cached value (abstract method).
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
key: Cache key
|
|
369
|
+
value: Value to cache
|
|
370
|
+
"""
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
def clear_cache(self) -> None:
|
|
374
|
+
"""Clear all cached values."""
|
|
375
|
+
with self._lock:
|
|
376
|
+
self._cache.clear()
|
|
377
|
+
|
|
378
|
+
@abstractmethod
|
|
379
|
+
def is_cache_valid(self, key: str) -> bool:
|
|
380
|
+
"""
|
|
381
|
+
Check if cache entry is still valid (abstract method).
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
key: Cache key
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
True if valid, False otherwise
|
|
388
|
+
"""
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# =============================================================================
|
|
393
|
+
# ABSTRACT LAZY LOADER (Proxy Pattern)
|
|
394
|
+
# =============================================================================
|
|
395
|
+
|
|
396
|
+
class ALazyLoader(ILazyLoader, ABC):
|
|
397
|
+
"""
|
|
398
|
+
Abstract base for lazy loading.
|
|
399
|
+
|
|
400
|
+
Implements Proxy pattern to defer module loading until first access.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
__slots__ = ('_module_path', '_cached_module', '_lock', '_loading')
|
|
404
|
+
|
|
405
|
+
def __init__(self, module_path: str):
|
|
406
|
+
"""
|
|
407
|
+
Initialize lazy loader.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
module_path: Full module path to load
|
|
411
|
+
"""
|
|
412
|
+
self._module_path = module_path
|
|
413
|
+
self._cached_module: Optional[ModuleType] = None
|
|
414
|
+
self._lock = threading.RLock()
|
|
415
|
+
self._loading = False
|
|
416
|
+
|
|
417
|
+
@abstractmethod
|
|
418
|
+
def load_module(self, module_path: str) -> ModuleType:
|
|
419
|
+
"""
|
|
420
|
+
Load a module lazily (abstract method).
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
module_path: Full module path to load
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Loaded module
|
|
427
|
+
"""
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
def is_loaded(self, module_path: str = None) -> bool:
|
|
431
|
+
"""
|
|
432
|
+
Check if module is already loaded.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
module_path: Module path to check (uses self._module_path if None)
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
True if loaded, False otherwise
|
|
439
|
+
"""
|
|
440
|
+
return self._cached_module is not None
|
|
441
|
+
|
|
442
|
+
@abstractmethod
|
|
443
|
+
def unload_module(self, module_path: str) -> None:
|
|
444
|
+
"""
|
|
445
|
+
Unload a module from cache (abstract method).
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
module_path: Module path to unload
|
|
449
|
+
"""
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# =============================================================================
|
|
454
|
+
# EXPORT ALL
|
|
455
|
+
# =============================================================================
|
|
456
|
+
|
|
457
|
+
__all__ = [
|
|
458
|
+
# Abstract base classes
|
|
459
|
+
'APackageDiscovery',
|
|
460
|
+
'APackageInstaller',
|
|
461
|
+
'AImportHook',
|
|
462
|
+
'APackageCache',
|
|
463
|
+
'ALazyLoader',
|
|
464
|
+
]
|
|
465
|
+
|