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,481 @@
1
+ """
2
+ Package Operations Facade
3
+
4
+ Main facade: XWPackageHelper extends APackageHelper
5
+ Provides concrete implementation for all package operations.
6
+ Uses strategy pattern for caching, helper, and manager strategies.
7
+ """
8
+
9
+ import sys
10
+ import subprocess
11
+ import importlib
12
+ import importlib.util
13
+ from typing import Optional
14
+
15
+ from .base import APackageHelper
16
+ from .services import InstallerEngine, LazyInstaller
17
+ from .services.strategy_registry import StrategyRegistry
18
+ from ..common.cache import InstallationCache
19
+ from ..common.logger import get_logger
20
+
21
+ logger = get_logger("xwlazy.package.facade")
22
+
23
+ # Import strategy interfaces
24
+ from ..contracts import (
25
+ ICachingStrategy,
26
+ IPackageHelperStrategy,
27
+ IPackageManagerStrategy,
28
+ IInstallExecutionStrategy,
29
+ IInstallTimingStrategy,
30
+ IDiscoveryStrategy,
31
+ IPolicyStrategy,
32
+ IMappingStrategy,
33
+ )
34
+
35
+ # Import default strategies
36
+ from ..common.strategies import InstallationCacheWrapper
37
+ from .strategies import (
38
+ # Execution strategies
39
+ PipExecution,
40
+ # Timing strategies
41
+ SmartTiming,
42
+ # Discovery strategies
43
+ HybridDiscovery,
44
+ # Policy strategies
45
+ PermissivePolicy,
46
+ # Mapping strategies
47
+ ManifestFirstMapping,
48
+ )
49
+
50
+
51
+ class XWPackageHelper(APackageHelper):
52
+ """
53
+ Concrete implementation of APackageHelper.
54
+
55
+ Provides simple, clean API for working with packages (what you pip install).
56
+ Uses xwlazy's InstallationCache for persistent caching and LazyInstaller for installation.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ package_name: str = 'default',
62
+ project_root: Optional[str] = None,
63
+ *,
64
+ # Legacy strategy injection (for backward compatibility)
65
+ caching_strategy: Optional[ICachingStrategy] = None,
66
+ helper_strategy: Optional[IPackageHelperStrategy] = None,
67
+ manager_strategy: Optional[IPackageManagerStrategy] = None,
68
+ # New strategy types
69
+ execution_strategy: Optional[IInstallExecutionStrategy] = None,
70
+ timing_strategy: Optional[IInstallTimingStrategy] = None,
71
+ discovery_strategy: Optional[IDiscoveryStrategy] = None,
72
+ policy_strategy: Optional[IPolicyStrategy] = None,
73
+ mapping_strategy: Optional[IMappingStrategy] = None,
74
+ ):
75
+ """
76
+ Initialize XW package helper.
77
+
78
+ Args:
79
+ package_name: Package name for isolation (defaults to 'default')
80
+ project_root: Root directory of project (auto-detected if None)
81
+ caching_strategy: Optional caching strategy. If None, uses InstallationCache.
82
+ helper_strategy: Optional helper strategy (legacy, deprecated).
83
+ manager_strategy: Optional manager strategy (legacy, deprecated).
84
+ execution_strategy: Optional execution strategy. If None, uses PipExecution.
85
+ timing_strategy: Optional timing strategy. If None, uses SmartTiming.
86
+ discovery_strategy: Optional discovery strategy. If None, uses HybridDiscovery.
87
+ policy_strategy: Optional policy strategy. If None, uses PermissivePolicy.
88
+ mapping_strategy: Optional mapping strategy. If None, uses ManifestFirstMapping.
89
+ """
90
+ super().__init__(package_name, project_root)
91
+
92
+ # Default strategies (legacy - deprecated, kept for backward compatibility)
93
+ if caching_strategy is None:
94
+ caching_strategy = InstallationCacheWrapper()
95
+ # Legacy helper_strategy and manager_strategy are deprecated
96
+ # They are kept for backward compatibility but not used
97
+
98
+ # Check registry for stored strategies, otherwise use defaults
99
+ if execution_strategy is None:
100
+ execution_strategy = StrategyRegistry.get_package_strategy(package_name, 'execution')
101
+ if execution_strategy is None:
102
+ execution_strategy = PipExecution()
103
+ if timing_strategy is None:
104
+ timing_strategy = StrategyRegistry.get_package_strategy(package_name, 'timing')
105
+ if timing_strategy is None:
106
+ timing_strategy = SmartTiming()
107
+ if discovery_strategy is None:
108
+ discovery_strategy = StrategyRegistry.get_package_strategy(package_name, 'discovery')
109
+ if discovery_strategy is None:
110
+ discovery_strategy = HybridDiscovery(package_name, project_root)
111
+ if policy_strategy is None:
112
+ policy_strategy = StrategyRegistry.get_package_strategy(package_name, 'policy')
113
+ if policy_strategy is None:
114
+ policy_strategy = PermissivePolicy()
115
+ if mapping_strategy is None:
116
+ mapping_strategy = StrategyRegistry.get_package_strategy(package_name, 'mapping')
117
+ if mapping_strategy is None:
118
+ mapping_strategy = ManifestFirstMapping(package_name)
119
+
120
+ # Store strategies
121
+ self._caching = caching_strategy
122
+ self._helper = helper_strategy # Legacy, deprecated
123
+ self._manager = manager_strategy # Legacy, deprecated
124
+ self._execution = execution_strategy
125
+ self._timing = timing_strategy
126
+ self._discovery = discovery_strategy
127
+ self._policy = policy_strategy
128
+ self._mapping = mapping_strategy
129
+
130
+ # Legacy support (for backward compatibility)
131
+ self._install_cache = InstallationCache()
132
+ self._installer = None # Lazy init to avoid circular imports
133
+ self._install_engine = InstallerEngine(package_name)
134
+
135
+ def _get_installer(self):
136
+ """Get lazy installer instance (lazy init)."""
137
+ if self._installer is None:
138
+ self._installer = LazyInstaller(self._package_name)
139
+ return self._installer
140
+
141
+ def _check_importability(self, package_name: str) -> bool:
142
+ """
143
+ Check if package is importable.
144
+
145
+ Uses importlib.util.find_spec to check if package can be imported.
146
+
147
+ Args:
148
+ package_name: Package name to check
149
+
150
+ Returns:
151
+ True if importable, False otherwise
152
+ """
153
+ try:
154
+ spec = importlib.util.find_spec(package_name)
155
+ return spec is not None
156
+ except (ValueError, AttributeError, ImportError):
157
+ return False
158
+
159
+ def _check_persistent_cache(self, package_name: str) -> bool:
160
+ """
161
+ Check persistent cache for package installation status.
162
+
163
+ Args:
164
+ package_name: Package name to check
165
+
166
+ Returns:
167
+ True if found in persistent cache as installed, False otherwise
168
+ """
169
+ return self._install_cache.is_installed(package_name)
170
+
171
+ def _mark_installed_in_persistent_cache(self, package_name: str) -> None:
172
+ """
173
+ Mark package as installed in persistent cache.
174
+
175
+ Args:
176
+ package_name: Package name to mark
177
+ """
178
+ version = self._get_installer()._get_installed_version(package_name)
179
+ self._install_cache.mark_installed(package_name, version)
180
+
181
+ def _mark_uninstalled_in_persistent_cache(self, package_name: str) -> None:
182
+ """
183
+ Mark package as uninstalled in persistent cache.
184
+
185
+ Args:
186
+ package_name: Package name to mark
187
+ """
188
+ self._install_cache.mark_uninstalled(package_name)
189
+
190
+ def _run_install(self, *package_names: str) -> None:
191
+ """
192
+ Run pip install for packages.
193
+
194
+ Uses execution strategy and timing strategy to determine when/how to install.
195
+
196
+ Args:
197
+ *package_names: Package names to install
198
+
199
+ Raises:
200
+ RuntimeError: If installation fails
201
+ """
202
+ if not package_names:
203
+ return
204
+
205
+ # Get policy args for each package
206
+ policy_args_map = {}
207
+ for package_name in package_names:
208
+ # Check if should install now (timing strategy)
209
+ if not self._timing.should_install_now(package_name, None):
210
+ continue
211
+
212
+ # Get pip args from policy strategy
213
+ policy_args = self._policy.get_pip_args(package_name)
214
+ policy_args_map[package_name] = policy_args
215
+
216
+ # Execute installations using execution strategy
217
+ for package_name, policy_args in policy_args_map.items():
218
+ result = self._execution.execute_install(package_name, policy_args)
219
+
220
+ # Handle result
221
+ if hasattr(result, 'success') and result.success:
222
+ with self._lock:
223
+ self._installed_packages.add(package_name)
224
+ self._uninstalled_packages.discard(package_name)
225
+ self._mark_installed_in_persistent_cache(package_name)
226
+ else:
227
+ with self._lock:
228
+ self._failed_packages.add(package_name)
229
+ error_msg = getattr(result, 'error', 'Unknown error') if hasattr(result, 'error') else str(result)
230
+ raise RuntimeError(f"Failed to install {package_name}: {error_msg}")
231
+
232
+ def _run_uninstall(self, *package_names: str) -> None:
233
+ """
234
+ Run pip uninstall for packages.
235
+
236
+ Uses execution strategy for uninstallation.
237
+
238
+ Args:
239
+ *package_names: Package names to uninstall
240
+
241
+ Raises:
242
+ RuntimeError: If uninstallation fails
243
+ """
244
+ if not package_names:
245
+ return
246
+
247
+ for package_name in package_names:
248
+ # Check if should uninstall (timing strategy)
249
+ if self._timing.should_uninstall_after(package_name, None):
250
+ success = self._execution.execute_uninstall(package_name)
251
+ if success:
252
+ with self._lock:
253
+ self._installed_packages.discard(package_name)
254
+ self._uninstalled_packages.add(package_name)
255
+ self._mark_uninstalled_in_persistent_cache(package_name)
256
+ else:
257
+ raise RuntimeError(f"Failed to uninstall {package_name}")
258
+
259
+ # Abstract methods from APackage that need implementation
260
+ def _discover_from_sources(self) -> None:
261
+ """Discover dependencies from all sources."""
262
+ # Use discovery strategy
263
+ deps = self._discovery.discover(self._project_root)
264
+ # Convert to DependencyInfo format
265
+ from ..defs import DependencyInfo
266
+ for import_name, package_name in deps.items():
267
+ self.discovered_dependencies[import_name] = DependencyInfo(
268
+ import_name=import_name,
269
+ package_name=package_name,
270
+ source=self._discovery.get_source(import_name) or 'discovery',
271
+ category='discovered'
272
+ )
273
+
274
+ def _is_cache_valid(self) -> bool:
275
+ """Check if cached dependencies are still valid."""
276
+ # Delegate to discovery strategy which has cache validation logic
277
+ if hasattr(self._discovery, '_is_cache_valid'):
278
+ return self._discovery._is_cache_valid()
279
+ # Fallback: if discovery doesn't support cache validation, assume invalid
280
+ return False
281
+
282
+ def _add_common_mappings(self) -> None:
283
+ """Add common import -> package mappings."""
284
+ # Use mapping strategy to discover mappings
285
+ # This is called during initialization to populate common mappings
286
+ # The mapping strategy handles this internally
287
+ pass
288
+
289
+ def _update_file_mtimes(self) -> None:
290
+ """Update file modification times for cache validation."""
291
+ # Delegate to discovery strategy which tracks file modification times
292
+ if hasattr(self._discovery, '_update_file_mtimes'):
293
+ self._discovery._update_file_mtimes()
294
+
295
+ # Strategy swapping methods
296
+ def swap_cache_strategy(self, new_strategy: ICachingStrategy) -> None:
297
+ """
298
+ Swap cache strategy at runtime.
299
+
300
+ Args:
301
+ new_strategy: New caching strategy to use
302
+ """
303
+ self._caching = new_strategy
304
+ # Update manager if it uses caching
305
+ if hasattr(self._manager, '_caching'):
306
+ self._manager._caching = new_strategy
307
+
308
+ def swap_helper_strategy(self, new_strategy: IPackageHelperStrategy) -> None:
309
+ """
310
+ Swap helper/installer strategy at runtime.
311
+
312
+ Args:
313
+ new_strategy: New helper strategy to use
314
+ """
315
+ self._helper = new_strategy
316
+ # Update manager if it uses helper
317
+ if hasattr(self._manager, '_helper'):
318
+ self._manager._helper = new_strategy
319
+
320
+ def swap_manager_strategy(self, new_strategy: IPackageManagerStrategy) -> None:
321
+ """
322
+ Swap manager strategy at runtime.
323
+
324
+ Args:
325
+ new_strategy: New manager strategy to use
326
+ """
327
+ self._manager = new_strategy
328
+
329
+ def swap_execution_strategy(self, new_strategy: IInstallExecutionStrategy) -> None:
330
+ """
331
+ Swap execution strategy at runtime.
332
+
333
+ Args:
334
+ new_strategy: New execution strategy to use
335
+ """
336
+ self._execution = new_strategy
337
+
338
+ def swap_timing_strategy(self, new_strategy: IInstallTimingStrategy) -> None:
339
+ """
340
+ Swap timing strategy at runtime.
341
+
342
+ Args:
343
+ new_strategy: New timing strategy to use
344
+ """
345
+ self._timing = new_strategy
346
+
347
+ def swap_discovery_strategy(self, new_strategy: IDiscoveryStrategy) -> None:
348
+ """
349
+ Swap discovery strategy at runtime.
350
+
351
+ Args:
352
+ new_strategy: New discovery strategy to use
353
+ """
354
+ self._discovery = new_strategy
355
+
356
+ def swap_policy_strategy(self, new_strategy: IPolicyStrategy) -> None:
357
+ """
358
+ Swap policy strategy at runtime.
359
+
360
+ Args:
361
+ new_strategy: New policy strategy to use
362
+ """
363
+ self._policy = new_strategy
364
+
365
+ def swap_mapping_strategy(self, new_strategy: IMappingStrategy) -> None:
366
+ """
367
+ Swap mapping strategy at runtime.
368
+
369
+ Args:
370
+ new_strategy: New mapping strategy to use
371
+ """
372
+ self._mapping = new_strategy
373
+
374
+ def install_package(self, package_name: str, module_name: Optional[str] = None) -> bool:
375
+ """
376
+ Install a package.
377
+
378
+ Uses timing strategy to determine if should install now,
379
+ then uses execution strategy to perform installation.
380
+
381
+ Args:
382
+ package_name: Package name to install
383
+ module_name: Optional module name (for mapping)
384
+
385
+ Returns:
386
+ True if installed successfully, False otherwise
387
+ """
388
+ # Map module name to package name if needed (using mapping strategy)
389
+ if module_name and not package_name:
390
+ package_name = self._mapping.map_import_to_package(module_name) or module_name
391
+
392
+ # Check timing strategy
393
+ if not self._timing.should_install_now(package_name, {'module_name': module_name}):
394
+ return False
395
+
396
+ # Check policy strategy
397
+ allowed, reason = self._policy.is_allowed(package_name)
398
+ if not allowed:
399
+ raise RuntimeError(f"Package {package_name} blocked by policy: {reason}")
400
+
401
+ # Get pip args from policy
402
+ policy_args = self._policy.get_pip_args(package_name)
403
+
404
+ # Execute installation
405
+ result = self._execution.execute_install(package_name, policy_args)
406
+
407
+ # Handle result
408
+ if hasattr(result, 'success') and result.success:
409
+ with self._lock:
410
+ self._installed_packages.add(package_name)
411
+ self._uninstalled_packages.discard(package_name)
412
+ self._mark_installed_in_persistent_cache(package_name)
413
+ return True
414
+ else:
415
+ with self._lock:
416
+ self._failed_packages.add(package_name)
417
+ return False
418
+
419
+ def _check_security_policy(self, package_name: str):
420
+ """Check security policy for package."""
421
+ # Use policy strategy
422
+ return self._policy.is_allowed(package_name)
423
+
424
+ def _run_pip_install(self, package_name: str, args: list) -> bool:
425
+ """Run pip install with arguments."""
426
+ try:
427
+ self._run_install(package_name)
428
+ return True
429
+ except (RuntimeError, subprocess.CalledProcessError, OSError) as e:
430
+ logger.debug(f"Failed to install {package_name}: {e}")
431
+ return False
432
+ except Exception as e:
433
+ # Catch-all for unexpected errors, but log them
434
+ logger.warning(f"Unexpected error installing {package_name}: {e}")
435
+ return False
436
+
437
+ def is_cache_valid(self, key: str) -> bool:
438
+ """Check if cache entry is still valid."""
439
+ # Use caching strategy if it supports validation
440
+ if hasattr(self._caching, 'is_valid') and self._caching is not None:
441
+ return self._caching.is_valid(key)
442
+ # Fallback: check if key exists in cache
443
+ return self.get_cached(key) is not None
444
+
445
+ # IConfigManager methods (delegate to LazyInstallConfig)
446
+ def is_enabled(self, package_name: str) -> bool:
447
+ """
448
+ Check if lazy install is enabled for a package (from IConfigManager).
449
+
450
+ This method delegates to LazyInstallConfig to avoid method name conflict
451
+ with the instance method is_enabled().
452
+
453
+ Args:
454
+ package_name: Package name to check
455
+
456
+ Returns:
457
+ True if enabled, False otherwise
458
+ """
459
+ from .services.config_manager import LazyInstallConfig
460
+ return LazyInstallConfig.is_enabled(package_name)
461
+
462
+ def get_mode(self, package_name: str) -> str:
463
+ """Get installation mode for a package (from IConfigManager)."""
464
+ from .services.config_manager import LazyInstallConfig
465
+ return LazyInstallConfig.get_mode(package_name)
466
+
467
+ def get_load_mode(self, package_name: str):
468
+ """Get load mode for a package (from IConfigManager)."""
469
+ from .services.config_manager import LazyInstallConfig
470
+ return LazyInstallConfig.get_load_mode(package_name)
471
+
472
+ def get_install_mode(self, package_name: str):
473
+ """Get install mode for a package (from IConfigManager)."""
474
+ from .services.config_manager import LazyInstallConfig
475
+ return LazyInstallConfig.get_install_mode(package_name)
476
+
477
+ def get_mode_config(self, package_name: str):
478
+ """Get full mode configuration for a package (from IConfigManager)."""
479
+ from .services.config_manager import LazyInstallConfig
480
+ return LazyInstallConfig.get_mode_config(package_name)
481
+
@@ -0,0 +1,84 @@
1
+ """
2
+ Package Services
3
+
4
+ Supporting services for package operations (not strategies).
5
+ """
6
+
7
+ from .install_result import InstallStatus, InstallResult
8
+ from .install_policy import LazyInstallPolicy
9
+ from .install_utils import get_trigger_file, is_externally_managed, check_pip_audit_available
10
+ from .install_registry import LazyInstallerRegistry
11
+ from .installer_engine import InstallerEngine
12
+ from .async_install_handle import AsyncInstallHandle
13
+ from .lazy_installer import LazyInstaller
14
+ from .strategy_registry import StrategyRegistry
15
+
16
+ # Config and manifest services
17
+ from .config_manager import LazyInstallConfig
18
+ from .manifest import (
19
+ PackageManifest,
20
+ LazyManifestLoader,
21
+ get_manifest_loader,
22
+ refresh_manifest_cache,
23
+ sync_manifest_configuration,
24
+ _normalize_prefix,
25
+ )
26
+
27
+ # Discovery service
28
+ from .discovery import LazyDiscovery, get_lazy_discovery
29
+
30
+ # State management and keyword detection (moved to common/services)
31
+ from ...common.services import (
32
+ LazyStateManager,
33
+ enable_keyword_detection,
34
+ is_keyword_detection_enabled,
35
+ get_keyword_detection_keyword,
36
+ check_package_keywords,
37
+ _detect_lazy_installation,
38
+ _detect_meta_info_mode,
39
+ )
40
+
41
+ # Host package registration
42
+ from .host_packages import (
43
+ register_host_package,
44
+ refresh_host_package,
45
+ )
46
+
47
+ __all__ = [
48
+ # Install services
49
+ 'InstallStatus',
50
+ 'InstallResult',
51
+ 'LazyInstallPolicy',
52
+ 'LazyInstallerRegistry',
53
+ 'InstallerEngine',
54
+ 'AsyncInstallHandle',
55
+ 'LazyInstaller',
56
+ 'StrategyRegistry',
57
+ 'get_trigger_file',
58
+ 'is_externally_managed',
59
+ 'check_pip_audit_available',
60
+ # Config and manifest
61
+ 'LazyInstallConfig',
62
+ 'PackageManifest',
63
+ 'LazyManifestLoader',
64
+ 'get_manifest_loader',
65
+ 'refresh_manifest_cache',
66
+ 'sync_manifest_configuration',
67
+ '_normalize_prefix',
68
+ # Discovery
69
+ 'LazyDiscovery',
70
+ 'get_lazy_discovery',
71
+ # State management
72
+ 'LazyStateManager',
73
+ # Keyword detection
74
+ 'enable_keyword_detection',
75
+ 'is_keyword_detection_enabled',
76
+ 'get_keyword_detection_keyword',
77
+ 'check_package_keywords',
78
+ '_detect_lazy_installation',
79
+ '_detect_meta_info_mode',
80
+ # Host package registration
81
+ 'register_host_package',
82
+ 'refresh_host_package',
83
+ ]
84
+
@@ -0,0 +1,89 @@
1
+ """
2
+ Async Install Handle
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
+ Lightweight handle for background installation jobs.
11
+ """
12
+
13
+ import asyncio
14
+ from typing import Optional, Any
15
+
16
+
17
+ class AsyncInstallHandle:
18
+ """Lightweight handle for background installation jobs."""
19
+
20
+ __slots__ = ("_task_or_future", "module_name", "package_name", "installer_package")
21
+
22
+ def __init__(
23
+ self,
24
+ task_or_future: Any, # Can be Future or asyncio.Task
25
+ module_name: str,
26
+ package_name: str,
27
+ installer_package: str,
28
+ ) -> None:
29
+ self._task_or_future = task_or_future
30
+ self.module_name = module_name
31
+ self.package_name = package_name
32
+ self.installer_package = installer_package
33
+
34
+ def wait(self, timeout: Optional[float] = None) -> bool:
35
+ """
36
+ Wait for installation to complete.
37
+
38
+ Args:
39
+ timeout: Optional timeout in seconds
40
+
41
+ Returns:
42
+ True if installation succeeded, False otherwise
43
+ """
44
+ try:
45
+ # Handle concurrent.futures.Future (from asyncio.run_coroutine_threadsafe)
46
+ if hasattr(self._task_or_future, 'result'):
47
+ result = self._task_or_future.result(timeout=timeout)
48
+ return bool(result)
49
+ # Handle asyncio.Task
50
+ elif hasattr(self._task_or_future, 'done'):
51
+ if timeout is None:
52
+ # Use asyncio.wait_for if we have a loop
53
+ try:
54
+ loop = asyncio.get_event_loop()
55
+ if loop.is_running():
56
+ # Can't wait in running loop, return False
57
+ return False
58
+ except RuntimeError:
59
+ pass
60
+ # Create new event loop to wait
61
+ return asyncio.run(self._wait_task())
62
+ else:
63
+ return asyncio.run(asyncio.wait_for(self._wait_task(), timeout=timeout))
64
+ return False
65
+ except Exception:
66
+ return False
67
+
68
+ async def _wait_task(self) -> bool:
69
+ """Async helper to wait for task."""
70
+ if hasattr(self._task_or_future, 'done'):
71
+ await self._task_or_future
72
+ return bool(self._task_or_future.result() if hasattr(self._task_or_future, 'result') else True)
73
+ return False
74
+
75
+ @property
76
+ def done(self) -> bool:
77
+ """
78
+ Check if installation is complete.
79
+
80
+ Returns:
81
+ True if installation is complete
82
+ """
83
+ if hasattr(self._task_or_future, 'done'):
84
+ return self._task_or_future.done()
85
+ return False
86
+
87
+
88
+ __all__ = ['AsyncInstallHandle']
89
+