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,397 @@
1
+ """
2
+ #exonware/xwlazy/src/exonware/xwlazy/defs.py
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+ Version: 0.1.0.19
8
+ Generation Date: 10-Oct-2025
9
+
10
+ Type Definitions and Constants for Lazy Loading System
11
+
12
+ This module defines type definitions, constants, and TypedDict structures
13
+ for the lazy loading system following GUIDE_ARCH.md structure.
14
+ """
15
+
16
+ from enum import Enum
17
+ from typing import TypedDict, Dict, List, Optional, Any, Tuple
18
+ from dataclasses import dataclass, field
19
+ from types import ModuleType
20
+
21
+
22
+ # =============================================================================
23
+ # ENUMS
24
+ # =============================================================================
25
+
26
+ class LazyLoadMode(Enum):
27
+ """Controls lazy module loading behavior."""
28
+ NONE = "none" # Standard imports (no lazy loading)
29
+ AUTO = "auto" # Lazy loading enabled (deferred module loading)
30
+ PRELOAD = "preload" # Preload all modules on start
31
+ BACKGROUND = "background" # Load modules in background threads
32
+ CACHED = "cached" # Cache loaded modules but allow unloading
33
+ # Superior performance modes
34
+ TURBO = "turbo" # Multi-tier cache + parallel preloading + bytecode caching
35
+ ADAPTIVE = "adaptive" # Self-optimizing with pattern learning
36
+ HYPERPARALLEL = "hyperparallel" # Maximum parallelism with multi-threading
37
+ STREAMING = "streaming" # Asynchronous background loading with streaming
38
+ ULTRA = "ultra" # Aggressive optimizations (pre-compiled bytecode, mmap, etc.)
39
+ INTELLIGENT = "intelligent" # Automatically switches to fastest mode based on load level
40
+
41
+
42
+ class LazyInstallMode(Enum):
43
+ """Lazy installation modes."""
44
+ # Core modes
45
+ NONE = "none" # No auto-installation
46
+ SMART = "smart" # Install on first usage (on-demand) - replaces AUTO
47
+ FULL = "full" # Install all dependencies on start
48
+ CLEAN = "clean" # Install on usage + uninstall after completion
49
+ TEMPORARY = "temporary" # Always uninstall after use (more aggressive than CLEAN)
50
+ SIZE_AWARE = "size_aware" # Install small packages, skip large ones
51
+
52
+ # Special purpose modes (kept for specific use cases)
53
+ INTERACTIVE = "interactive" # Ask user before installing
54
+ WARN = "warn" # Log warning but don't install (for monitoring)
55
+ DISABLED = "disabled" # Don't install anything (alias for NONE, more explicit)
56
+ DRY_RUN = "dry_run" # Show what would be installed but don't install
57
+
58
+
59
+ class PathType(Enum):
60
+ """Path types for validation."""
61
+ FILE = "file"
62
+ DIRECTORY = "directory"
63
+ UNKNOWN = "unknown"
64
+
65
+
66
+ class InstallStatus(Enum):
67
+ """Installation status."""
68
+ PENDING = "pending"
69
+ RUNNING = "running"
70
+ SUCCESS = "success"
71
+ FAILED = "failed"
72
+ SKIPPED = "skipped"
73
+
74
+
75
+ class LoadLevel(Enum):
76
+ """Load level categories."""
77
+ LIGHT = "light_load"
78
+ MEDIUM = "medium_load"
79
+ HEAVY = "heavy_load"
80
+ ENTERPRISE = "enterprise_load"
81
+
82
+
83
+ # =============================================================================
84
+ # DATACLASSES
85
+ # =============================================================================
86
+
87
+ @dataclass
88
+ class DependencyInfo:
89
+ """Information about a discovered dependency."""
90
+ import_name: str
91
+ package_name: str
92
+ version: Optional[str] = None
93
+ source: str = "unknown"
94
+ category: str = "general"
95
+
96
+
97
+ @dataclass
98
+ class LazyModeConfig:
99
+ """Two-dimensional lazy mode configuration combining load and install modes."""
100
+ load_mode: LazyLoadMode = LazyLoadMode.NONE
101
+ install_mode: LazyInstallMode = LazyInstallMode.NONE
102
+
103
+ # Additional configuration options
104
+ auto_uninstall_large: bool = False # For AUTO_MODE behavior
105
+ large_package_threshold_mb: float = 50.0 # Size threshold for SIZE_AWARE mode
106
+ preload_priority: List[str] = field(default_factory=list) # Priority modules for PRELOAD
107
+ background_workers: int = 2 # Workers for BACKGROUND mode
108
+
109
+ def __post_init__(self):
110
+ """Normalize enum values."""
111
+ if isinstance(self.load_mode, str):
112
+ self.load_mode = LazyLoadMode(self.load_mode)
113
+ if isinstance(self.install_mode, str):
114
+ self.install_mode = LazyInstallMode(self.install_mode)
115
+
116
+
117
+ @dataclass
118
+ class InstallResult:
119
+ """Result of an installation operation."""
120
+ package_name: str
121
+ success: bool
122
+ status: InstallStatus
123
+ error: Optional[str] = None
124
+ version: Optional[str] = None
125
+ source: Optional[str] = None # "cache", "pip", "wheel", etc.
126
+
127
+
128
+ @dataclass
129
+ class LazyConfig:
130
+ """Bridge configuration settings with the lazy package implementation."""
131
+ packages: Tuple[str, ...] = field(
132
+ default_factory=lambda: ("default",)
133
+ )
134
+
135
+ def __post_init__(self) -> None:
136
+ self.packages = tuple(package.lower() for package in self.packages)
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class PackageManifest:
141
+ """Resolved manifest data for a single package."""
142
+ package: str
143
+ dependencies: Dict[str, str] = field(default_factory=dict)
144
+ watched_prefixes: Tuple[str, ...] = ()
145
+ async_installs: bool = False
146
+ async_workers: int = 1
147
+ class_wrap_prefixes: Tuple[str, ...] = ()
148
+ metadata: Dict[str, Any] = field(default_factory=dict)
149
+
150
+ def get_dependency(self, import_name: str) -> Optional[str]:
151
+ """Return the declared package for the given import name."""
152
+ if not import_name:
153
+ return None
154
+ direct = self.dependencies.get(import_name)
155
+ if direct is not None:
156
+ return direct
157
+ # Case-insensitive fallback for convenience
158
+ return self.dependencies.get(import_name.lower())
159
+
160
+
161
+ @dataclass(frozen=True)
162
+ class PackageData:
163
+ """
164
+ Immutable package data - same across all strategies.
165
+
166
+ This data structure is used by all package caching, helper, and manager strategies.
167
+ """
168
+ name: str
169
+ version: Optional[str] = None
170
+ installed: bool = False
171
+ install_time: Optional[float] = None
172
+ access_count: int = 0
173
+ install_mode: Optional['LazyInstallMode'] = None
174
+ error: Optional[Exception] = None
175
+ metadata: Dict[str, Any] = field(default_factory=dict)
176
+
177
+
178
+ @dataclass(frozen=True)
179
+ class ModuleData:
180
+ """
181
+ Immutable module data - same across all strategies.
182
+
183
+ This data structure is used by all module caching, helper, and manager strategies.
184
+ """
185
+ path: str
186
+ loaded_module: Optional['ModuleType'] = None
187
+ loading: bool = False
188
+ load_time: Optional[float] = None
189
+ access_count: int = 0
190
+ error: Optional[Exception] = None
191
+ metadata: Dict[str, Any] = field(default_factory=dict)
192
+
193
+
194
+ # =============================================================================
195
+ # TYPE DEFINITIONS
196
+ # =============================================================================
197
+
198
+ class DependencyMapping(TypedDict, total=False):
199
+ """Type definition for dependency mapping structure."""
200
+ import_name: str
201
+ package_name: str
202
+ version: Optional[str]
203
+ source: str
204
+ category: str
205
+
206
+
207
+ class PackageStats(TypedDict, total=False):
208
+ """Type definition for package statistics."""
209
+ enabled: bool
210
+ mode: str
211
+ package_name: str
212
+ installed_packages: List[str]
213
+ failed_packages: List[str]
214
+ total_installed: int
215
+ total_failed: int
216
+
217
+
218
+ class LazyStatus(TypedDict, total=False):
219
+ """Type definition for lazy mode status."""
220
+ enabled: bool
221
+ hook_installed: bool
222
+ lazy_install_enabled: bool
223
+ active: bool
224
+ error: Optional[str]
225
+
226
+
227
+ # =============================================================================
228
+ # CONSTANTS
229
+ # =============================================================================
230
+
231
+ # Default configuration values
232
+ DEFAULT_LARGE_PACKAGE_THRESHOLD_MB: float = 50.0
233
+ DEFAULT_BACKGROUND_WORKERS: int = 2
234
+ DEFAULT_PRELOAD_PRIORITY: List[str] = []
235
+
236
+ # Common import -> package mappings (will be populated from discovery)
237
+ COMMON_IMPORT_MAPPINGS: Dict[str, str] = {
238
+ # Common mappings that are frequently used
239
+ 'cv2': 'opencv-python',
240
+ 'PIL': 'Pillow',
241
+ 'yaml': 'PyYAML',
242
+ 'toml': 'toml',
243
+ 'pandas': 'pandas',
244
+ 'numpy': 'numpy',
245
+ 'scipy': 'scipy',
246
+ 'sklearn': 'scikit-learn',
247
+ 'matplotlib': 'matplotlib',
248
+ 'seaborn': 'seaborn',
249
+ 'requests': 'requests',
250
+ 'urllib3': 'urllib3',
251
+ 'bs4': 'beautifulsoup4',
252
+ 'lxml': 'lxml',
253
+ 'jinja2': 'Jinja2',
254
+ 'flask': 'Flask',
255
+ 'django': 'Django',
256
+ 'fastapi': 'fastapi',
257
+ 'pydantic': 'pydantic',
258
+ 'sqlalchemy': 'SQLAlchemy',
259
+ 'psycopg2': 'psycopg2-binary',
260
+ 'pymongo': 'pymongo',
261
+ 'redis': 'redis',
262
+ 'celery': 'celery',
263
+ 'boto3': 'boto3',
264
+ 'azure': 'azure-storage-blob',
265
+ 'google': 'google-cloud-storage',
266
+ 'tensorflow': 'tensorflow',
267
+ 'torch': 'torch',
268
+ 'transformers': 'transformers',
269
+ 'openai': 'openai',
270
+ 'anthropic': 'anthropic',
271
+ }
272
+
273
+ # Package discovery source names
274
+ DISCOVERY_SOURCE_PYPROJECT = "pyproject.toml"
275
+ DISCOVERY_SOURCE_REQUIREMENTS = "requirements.txt"
276
+ DISCOVERY_SOURCE_SETUP = "setup.py"
277
+ DISCOVERY_SOURCE_POETRY = "poetry.lock"
278
+ DISCOVERY_SOURCE_PIPFILE = "Pipfile"
279
+ DISCOVERY_SOURCE_MANIFEST = "manifest.json"
280
+
281
+ # Installation mode aliases
282
+ INSTALL_MODE_ALIASES: Dict[str, str] = {
283
+ 'auto': 'smart',
284
+ 'on_demand': 'smart',
285
+ 'on-demand': 'smart',
286
+ 'lazy': 'smart',
287
+ }
288
+
289
+ # Cache keys
290
+ CACHE_KEY_DEPENDENCIES = "dependencies"
291
+ CACHE_KEY_PACKAGE_INFO = "package_info"
292
+ CACHE_KEY_INSTALL_STATUS = "install_status"
293
+ CACHE_KEY_DISCOVERY_SOURCES = "discovery_sources"
294
+
295
+ # File patterns for discovery
296
+ PYPROJECT_PATTERN = "pyproject.toml"
297
+ REQUIREMENTS_PATTERN = "requirements*.txt"
298
+ SETUP_PATTERN = "setup.py"
299
+ POETRY_PATTERN = "poetry.lock"
300
+ PIPFILE_PATTERN = "Pipfile"
301
+
302
+ # =============================================================================
303
+ # PRESET MODE MAPPINGS
304
+ # =============================================================================
305
+
306
+ # Preset mode combinations for convenience
307
+ PRESET_MODES: Dict[str, LazyModeConfig] = {
308
+ "none": LazyModeConfig(
309
+ load_mode=LazyLoadMode.NONE,
310
+ install_mode=LazyInstallMode.NONE
311
+ ),
312
+ "lite": LazyModeConfig(
313
+ load_mode=LazyLoadMode.AUTO,
314
+ install_mode=LazyInstallMode.NONE
315
+ ),
316
+ "smart": LazyModeConfig(
317
+ load_mode=LazyLoadMode.AUTO,
318
+ install_mode=LazyInstallMode.SMART
319
+ ),
320
+ "full": LazyModeConfig(
321
+ load_mode=LazyLoadMode.AUTO,
322
+ install_mode=LazyInstallMode.FULL
323
+ ),
324
+ "clean": LazyModeConfig(
325
+ load_mode=LazyLoadMode.AUTO,
326
+ install_mode=LazyInstallMode.CLEAN
327
+ ),
328
+ "temporary": LazyModeConfig(
329
+ load_mode=LazyLoadMode.AUTO,
330
+ install_mode=LazyInstallMode.TEMPORARY
331
+ ),
332
+ "size_aware": LazyModeConfig(
333
+ load_mode=LazyLoadMode.AUTO,
334
+ install_mode=LazyInstallMode.SIZE_AWARE
335
+ ),
336
+ "auto": LazyModeConfig(
337
+ load_mode=LazyLoadMode.AUTO,
338
+ install_mode=LazyInstallMode.SMART,
339
+ auto_uninstall_large=True
340
+ ),
341
+ }
342
+
343
+
344
+ def get_preset_mode(preset_name: str) -> Optional[LazyModeConfig]:
345
+ """Get preset mode configuration by name."""
346
+ return PRESET_MODES.get(preset_name.lower())
347
+
348
+
349
+ # =============================================================================
350
+ # EXPORT ALL
351
+ # =============================================================================
352
+
353
+ __all__ = [
354
+ # Enums
355
+ 'LazyLoadMode',
356
+ 'LazyInstallMode',
357
+ 'PathType',
358
+ 'InstallStatus',
359
+ 'LoadLevel',
360
+ # Dataclasses
361
+ 'DependencyInfo',
362
+ 'LazyModeConfig',
363
+ 'InstallResult',
364
+ 'LazyConfig',
365
+ 'PackageManifest',
366
+ 'PackageData',
367
+ 'ModuleData',
368
+ # Type definitions
369
+ 'DependencyMapping',
370
+ 'PackageStats',
371
+ 'LazyStatus',
372
+ # Constants
373
+ 'DEFAULT_LARGE_PACKAGE_THRESHOLD_MB',
374
+ 'DEFAULT_BACKGROUND_WORKERS',
375
+ 'DEFAULT_PRELOAD_PRIORITY',
376
+ 'COMMON_IMPORT_MAPPINGS',
377
+ 'DISCOVERY_SOURCE_PYPROJECT',
378
+ 'DISCOVERY_SOURCE_REQUIREMENTS',
379
+ 'DISCOVERY_SOURCE_SETUP',
380
+ 'DISCOVERY_SOURCE_POETRY',
381
+ 'DISCOVERY_SOURCE_PIPFILE',
382
+ 'DISCOVERY_SOURCE_MANIFEST',
383
+ 'INSTALL_MODE_ALIASES',
384
+ 'CACHE_KEY_DEPENDENCIES',
385
+ 'CACHE_KEY_PACKAGE_INFO',
386
+ 'CACHE_KEY_INSTALL_STATUS',
387
+ 'CACHE_KEY_DISCOVERY_SOURCES',
388
+ 'PYPROJECT_PATTERN',
389
+ 'REQUIREMENTS_PATTERN',
390
+ 'SETUP_PATTERN',
391
+ 'POETRY_PATTERN',
392
+ 'PIPFILE_PATTERN',
393
+ # Preset modes
394
+ 'PRESET_MODES',
395
+ 'get_preset_mode',
396
+ ]
397
+
@@ -0,0 +1,284 @@
1
+ """
2
+ #exonware/xwlazy/src/exonware/xwlazy/errors.py
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+ Version: 0.1.0.19
8
+ Generation Date: 10-Oct-2025
9
+
10
+ Errors for Lazy Loading System
11
+
12
+ This module defines all exception classes for the lazy loading system
13
+ following GUIDE_ARCH.md structure.
14
+ """
15
+
16
+ from typing import Optional, Any
17
+
18
+
19
+ # =============================================================================
20
+ # BASE EXCEPTION
21
+ # =============================================================================
22
+
23
+ class LazySystemError(Exception):
24
+ """
25
+ Base exception for all lazy system errors.
26
+
27
+ All lazy system exceptions inherit from this for easy error handling.
28
+ """
29
+
30
+ def __init__(self, message: str, package_name: Optional[str] = None):
31
+ """
32
+ Initialize lazy system error.
33
+
34
+ Args:
35
+ message: Error message
36
+ package_name: Optional package name for scoped errors
37
+ """
38
+ self.package_name = package_name
39
+ if package_name:
40
+ message = f"[{package_name}] {message}"
41
+ super().__init__(message)
42
+
43
+
44
+ # =============================================================================
45
+ # SPECIFIC EXCEPTIONS
46
+ # =============================================================================
47
+
48
+ class LazyInstallError(LazySystemError):
49
+ """
50
+ Raised when package installation fails.
51
+
52
+ Examples:
53
+ - pip install command fails
54
+ - Package not found in PyPI
55
+ - Network error during installation
56
+ """
57
+ pass
58
+
59
+
60
+ class LazyDiscoveryError(LazySystemError):
61
+ """
62
+ Raised when dependency discovery fails.
63
+
64
+ Examples:
65
+ - Cannot read pyproject.toml
66
+ - Invalid TOML syntax
67
+ - Missing dependency configuration
68
+ """
69
+ pass
70
+
71
+
72
+ class LazyHookError(LazySystemError):
73
+ """
74
+ Raised when import hook operation fails.
75
+
76
+ Examples:
77
+ - Cannot install hook in sys.meta_path
78
+ - Hook is already installed
79
+ - Hook interception fails
80
+ """
81
+ pass
82
+
83
+
84
+ class LazySecurityError(LazySystemError):
85
+ """
86
+ Raised when security policy is violated.
87
+
88
+ Examples:
89
+ - Package not in allow list
90
+ - Package in deny list
91
+ - Untrusted package source
92
+ """
93
+ pass
94
+
95
+
96
+ class ExternallyManagedError(LazyInstallError):
97
+ """
98
+ Raised when environment is externally managed (PEP 668).
99
+
100
+ This happens when the Python environment has an EXTERNALLY-MANAGED
101
+ marker file, preventing pip installations. Common in system Python
102
+ installations on Linux distributions.
103
+
104
+ Solutions:
105
+ 1. Use a virtual environment
106
+ 2. Use pipx for isolated installations
107
+ 3. Override with --break-system-packages (not recommended)
108
+ """
109
+
110
+ def __init__(self, package_name: str):
111
+ """
112
+ Initialize externally managed error.
113
+
114
+ Args:
115
+ package_name: Package that cannot be installed
116
+ """
117
+ message = (
118
+ f"Cannot install '{package_name}': Environment is externally managed (PEP 668). "
119
+ f"Please use a virtual environment or pipx."
120
+ )
121
+ super().__init__(message, package_name=None)
122
+
123
+
124
+ class DeferredImportError(Exception):
125
+ """
126
+ Placeholder for a failed import that will be retried when accessed.
127
+
128
+ This enables two-stage lazy loading:
129
+ - Stage 1: Import fails → Return DeferredImportError placeholder
130
+ - Stage 2: On first use → Install missing package and replace with real module
131
+
132
+ Performance optimized:
133
+ - Zero overhead until user actually accesses the deferred import
134
+ - Only installs dependencies when truly needed
135
+ - Caches successful imports to avoid repeated installs
136
+
137
+ Note: This is both an error class and a proxy object. It stays in
138
+ errors.py because it represents an error state, but acts as
139
+ a proxy until resolved.
140
+ """
141
+
142
+ __slots__ = (
143
+ '_import_name',
144
+ '_original_error',
145
+ '_installer_package',
146
+ '_retry_attempted',
147
+ '_real_module',
148
+ '_async_handle',
149
+ )
150
+
151
+ def __init__(
152
+ self,
153
+ import_name: str,
154
+ original_error: Exception,
155
+ installer_package: str,
156
+ async_handle: Optional[Any] = None,
157
+ ):
158
+ """
159
+ Initialize deferred import placeholder.
160
+
161
+ Args:
162
+ import_name: Name of the module that failed to import (e.g., 'fastavro')
163
+ original_error: The original ImportError that was caught
164
+ installer_package: Package name to use for lazy installation (e.g., 'xwsystem')
165
+ """
166
+ self._import_name = import_name
167
+ self._original_error = original_error
168
+ self._installer_package = installer_package
169
+ self._retry_attempted = False
170
+ self._real_module = None
171
+ self._async_handle = async_handle
172
+ super().__init__(f"Deferred import: {import_name}")
173
+
174
+ def _try_install_and_import(self):
175
+ """
176
+ Attempt to install missing package and import it.
177
+
178
+ Returns:
179
+ The real module if installation succeeds
180
+
181
+ Raises:
182
+ Original ImportError if installation fails or is disabled
183
+ """
184
+ # Import from facade and new structure
185
+ from .facade import lazy_import_with_install, is_lazy_install_enabled
186
+ from .common.logger import get_logger
187
+
188
+ logger = get_logger("xwlazy")
189
+ logger.info(f"[STAGE 2] _try_install_and_import called for '{self._import_name}'")
190
+
191
+ # Return cached module if already installed
192
+ if self._real_module is not None:
193
+ logger.info(f"[STAGE 2] Using cached module for '{self._import_name}'")
194
+ return self._real_module
195
+
196
+ # Only try once to avoid repeated failures
197
+ if self._retry_attempted:
198
+ logger.warning(f"[STAGE 2] Already attempted installation for '{self._import_name}', raising original error")
199
+ raise self._original_error
200
+
201
+ self._retry_attempted = True
202
+
203
+ if self._async_handle is not None:
204
+ logger.info(f"[STAGE 2] Waiting for async install of '{self._import_name}' to finish")
205
+ self._async_handle.wait()
206
+
207
+ # After async install, try simple import first (cache invalidation needed)
208
+ try:
209
+ import importlib
210
+ import sys
211
+ importlib.invalidate_caches()
212
+ sys.path_importer_cache.clear()
213
+ module = importlib.import_module(self._import_name)
214
+ self._real_module = module
215
+ logger.info(f"✅ [STAGE 2] Successfully loaded '{self._import_name}' after async install")
216
+ return module
217
+ except ImportError:
218
+ pass # Fall through to lazy_import_with_install
219
+
220
+ if not is_lazy_install_enabled(self._installer_package):
221
+ logger.warning(f"[STAGE 2] Lazy install disabled for {self._installer_package}, cannot load {self._import_name}")
222
+ raise self._original_error
223
+
224
+ logger.info(f"⏳ [STAGE 2] Installing '{self._import_name}' on first use...")
225
+
226
+ # Try to install and import
227
+ module, success = lazy_import_with_install(
228
+ self._import_name,
229
+ installer_package=self._installer_package
230
+ )
231
+
232
+ if success and module:
233
+ self._real_module = module
234
+ logger.info(f"✅ [STAGE 2] Successfully installed and loaded '{self._import_name}'")
235
+ return module
236
+ else:
237
+ logger.error(f"❌ [STAGE 2] Failed to install '{self._import_name}'")
238
+ raise self._original_error
239
+
240
+ def __call__(self, *args, **kwargs):
241
+ """
242
+ When user tries to instantiate, install dependency first.
243
+
244
+ This enables: serializer = AvroSerializer() → installs fastavro → creates instance
245
+ """
246
+ module = self._try_install_and_import()
247
+ # If module is callable (a class), instantiate it
248
+ if callable(module):
249
+ return module(*args, **kwargs)
250
+ return module
251
+
252
+ def __getattr__(self, name):
253
+ """
254
+ When user accesses attributes, install dependency first.
255
+
256
+ This enables: from fastavro import reader → installs fastavro → returns reader
257
+ """
258
+ module = self._try_install_and_import()
259
+ return getattr(module, name)
260
+
261
+ def __repr__(self):
262
+ """Show helpful message about deferred import."""
263
+ if self._real_module is not None:
264
+ return f"<DeferredImport: {self._import_name} (loaded)>"
265
+ return f"<DeferredImport: {self._import_name} (will install on first use)>"
266
+
267
+
268
+ # =============================================================================
269
+ # EXPORT ALL
270
+ # =============================================================================
271
+
272
+ __all__ = [
273
+ # Base exception
274
+ 'LazySystemError',
275
+ # Specific exceptions
276
+ 'LazyInstallError',
277
+ 'LazyDiscoveryError',
278
+ 'LazyHookError',
279
+ 'LazySecurityError',
280
+ 'ExternallyManagedError',
281
+ # Two-stage loading
282
+ 'DeferredImportError',
283
+ ]
284
+