exonware-xwlazy 0.1.0.11__py3-none-any.whl → 0.1.0.20__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 (96) hide show
  1. exonware/__init__.py +26 -0
  2. exonware/xwlazy/__init__.py +0 -0
  3. exonware/xwlazy/common/__init__.py +47 -0
  4. exonware/xwlazy/common/base.py +56 -0
  5. exonware/xwlazy/common/cache.py +504 -0
  6. exonware/xwlazy/common/logger.py +257 -0
  7. exonware/xwlazy/common/services/__init__.py +72 -0
  8. exonware/xwlazy/common/services/dependency_mapper.py +232 -0
  9. exonware/xwlazy/common/services/install_async_utils.py +165 -0
  10. exonware/xwlazy/common/services/install_cache_utils.py +245 -0
  11. exonware/xwlazy/common/services/keyword_detection.py +283 -0
  12. exonware/xwlazy/common/services/spec_cache.py +165 -0
  13. xwlazy/lazy/lazy_state.py → exonware/xwlazy/common/services/state_manager.py +0 -2
  14. exonware/xwlazy/common/strategies/__init__.py +28 -0
  15. exonware/xwlazy/common/strategies/caching_dict.py +44 -0
  16. exonware/xwlazy/common/strategies/caching_installation.py +88 -0
  17. exonware/xwlazy/common/strategies/caching_lfu.py +66 -0
  18. exonware/xwlazy/common/strategies/caching_lru.py +63 -0
  19. exonware/xwlazy/common/strategies/caching_multitier.py +59 -0
  20. exonware/xwlazy/common/strategies/caching_ttl.py +59 -0
  21. {xwlazy/lazy → exonware/xwlazy}/config.py +51 -21
  22. exonware/xwlazy/contracts.py +1396 -0
  23. exonware/xwlazy/defs.py +378 -0
  24. xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -16
  25. exonware/xwlazy/facade.py +991 -0
  26. exonware/xwlazy/module/__init__.py +18 -0
  27. exonware/xwlazy/module/base.py +565 -0
  28. exonware/xwlazy/module/data.py +17 -0
  29. exonware/xwlazy/module/facade.py +246 -0
  30. exonware/xwlazy/module/importer_engine.py +2117 -0
  31. exonware/xwlazy/module/strategies/__init__.py +22 -0
  32. exonware/xwlazy/module/strategies/module_helper_lazy.py +93 -0
  33. exonware/xwlazy/module/strategies/module_helper_simple.py +65 -0
  34. exonware/xwlazy/module/strategies/module_manager_advanced.py +111 -0
  35. exonware/xwlazy/module/strategies/module_manager_simple.py +95 -0
  36. exonware/xwlazy/package/__init__.py +18 -0
  37. exonware/xwlazy/package/base.py +798 -0
  38. xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +61 -16
  39. exonware/xwlazy/package/data.py +17 -0
  40. exonware/xwlazy/package/facade.py +480 -0
  41. exonware/xwlazy/package/services/__init__.py +84 -0
  42. exonware/xwlazy/package/services/async_install_handle.py +87 -0
  43. exonware/xwlazy/package/services/config_manager.py +245 -0
  44. exonware/xwlazy/package/services/discovery.py +370 -0
  45. {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -20
  46. exonware/xwlazy/package/services/install_async.py +277 -0
  47. exonware/xwlazy/package/services/install_cache.py +145 -0
  48. exonware/xwlazy/package/services/install_interactive.py +59 -0
  49. exonware/xwlazy/package/services/install_policy.py +156 -0
  50. exonware/xwlazy/package/services/install_registry.py +54 -0
  51. exonware/xwlazy/package/services/install_result.py +17 -0
  52. exonware/xwlazy/package/services/install_sbom.py +153 -0
  53. exonware/xwlazy/package/services/install_utils.py +79 -0
  54. exonware/xwlazy/package/services/installer_engine.py +406 -0
  55. exonware/xwlazy/package/services/lazy_installer.py +718 -0
  56. {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +40 -33
  57. exonware/xwlazy/package/services/strategy_registry.py +186 -0
  58. exonware/xwlazy/package/strategies/__init__.py +57 -0
  59. exonware/xwlazy/package/strategies/package_discovery_file.py +129 -0
  60. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +84 -0
  61. exonware/xwlazy/package/strategies/package_discovery_manifest.py +101 -0
  62. exonware/xwlazy/package/strategies/package_execution_async.py +113 -0
  63. exonware/xwlazy/package/strategies/package_execution_cached.py +90 -0
  64. exonware/xwlazy/package/strategies/package_execution_pip.py +99 -0
  65. exonware/xwlazy/package/strategies/package_execution_wheel.py +106 -0
  66. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +100 -0
  67. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +105 -0
  68. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +100 -0
  69. exonware/xwlazy/package/strategies/package_policy_allow_list.py +57 -0
  70. exonware/xwlazy/package/strategies/package_policy_deny_list.py +57 -0
  71. exonware/xwlazy/package/strategies/package_policy_permissive.py +46 -0
  72. exonware/xwlazy/package/strategies/package_timing_clean.py +67 -0
  73. exonware/xwlazy/package/strategies/package_timing_full.py +66 -0
  74. exonware/xwlazy/package/strategies/package_timing_smart.py +68 -0
  75. exonware/xwlazy/package/strategies/package_timing_temporary.py +66 -0
  76. exonware/xwlazy/runtime/__init__.py +18 -0
  77. exonware/xwlazy/runtime/adaptive_learner.py +129 -0
  78. exonware/xwlazy/runtime/base.py +274 -0
  79. exonware/xwlazy/runtime/facade.py +94 -0
  80. exonware/xwlazy/runtime/intelligent_selector.py +170 -0
  81. exonware/xwlazy/runtime/metrics.py +60 -0
  82. exonware/xwlazy/runtime/performance.py +37 -0
  83. exonware/xwlazy/version.py +2 -2
  84. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/METADATA +89 -11
  85. exonware_xwlazy-0.1.0.20.dist-info/RECORD +87 -0
  86. exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
  87. xwlazy/__init__.py +0 -34
  88. xwlazy/lazy/__init__.py +0 -301
  89. xwlazy/lazy/bootstrap.py +0 -106
  90. xwlazy/lazy/lazy_base.py +0 -465
  91. xwlazy/lazy/lazy_contracts.py +0 -290
  92. xwlazy/lazy/lazy_core.py +0 -3727
  93. xwlazy/lazy/logging_utils.py +0 -194
  94. xwlazy/version.py +0 -77
  95. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/WHEEL +0 -0
  96. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,798 @@
1
+ """
2
+ #exonware/xwlazy/src/exonware/xwlazy/package/base.py
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+
8
+ Generation Date: 10-Oct-2025
9
+
10
+ Abstract Base Class for Package Operations
11
+
12
+ This module defines the abstract base class for package operations.
13
+ """
14
+
15
+ import threading
16
+ from abc import ABC, abstractmethod
17
+ from pathlib import Path
18
+ from typing import Dict, List, Optional, Any, Set, Tuple
19
+ from types import ModuleType
20
+
21
+ from ..defs import (
22
+ DependencyInfo,
23
+ LazyInstallMode,
24
+ )
25
+ from ..contracts import (
26
+ IPackageHelper,
27
+ IPackageHelperStrategy,
28
+ IPackageManagerStrategy,
29
+ IInstallExecutionStrategy,
30
+ IInstallTimingStrategy,
31
+ IDiscoveryStrategy,
32
+ IPolicyStrategy,
33
+ IMappingStrategy,
34
+ )
35
+
36
+ # =============================================================================
37
+ # ABSTRACT PACKAGE (Unified - Merges APackageDiscovery + APackageInstaller + APackageCache + APackageHelper)
38
+ # =============================================================================
39
+
40
+ class APackageHelper(IPackageHelper, ABC):
41
+ """
42
+ Unified abstract base for package operations.
43
+
44
+ Merges functionality from APackageDiscovery, APackageInstaller, APackageCache, and APackageHelper.
45
+ Provides comprehensive package operations: discovery, installation, caching, configuration, manifest loading, and dependency mapping.
46
+
47
+ This abstract class combines:
48
+ - Package discovery (mapping import names to package names)
49
+ - Package installation (installing/uninstalling packages)
50
+ - Package caching (caching installation status and metadata)
51
+ - Configuration management (per-package lazy installation configuration)
52
+ - Manifest loading (loading and caching dependency manifests)
53
+ - Dependency mapping (mapping import names to package names)
54
+ """
55
+
56
+ __slots__ = (
57
+ # From APackageDiscovery
58
+ 'project_root', 'discovered_dependencies', '_discovery_sources',
59
+ '_cached_dependencies', '_file_mtimes', '_cache_valid',
60
+ # From APackageInstaller
61
+ '_package_name', '_enabled', '_mode', '_installed_packages',
62
+ '_failed_packages',
63
+ # From APackageCache
64
+ '_cache',
65
+ # From APackageHelper
66
+ '_uninstalled_packages',
67
+ # Common
68
+ '_lock'
69
+ )
70
+
71
+ def __init__(self, package_name: str = 'default', project_root: Optional[str] = None):
72
+ """
73
+ Initialize unified package operations.
74
+
75
+ Args:
76
+ package_name: Name of package this instance is for (for isolation)
77
+ project_root: Root directory of project (auto-detected if None)
78
+ """
79
+ # From APackageDiscovery
80
+ self.project_root = Path(project_root) if project_root else self._find_project_root()
81
+ self.discovered_dependencies: Dict[str, DependencyInfo] = {}
82
+ self._discovery_sources: List[str] = []
83
+ self._cached_dependencies: Dict[str, str] = {}
84
+ self._file_mtimes: Dict[str, float] = {}
85
+ self._cache_valid = False
86
+
87
+ # From APackageInstaller
88
+ self._package_name = package_name
89
+ self._enabled = False
90
+ self._mode = LazyInstallMode.SMART
91
+ self._installed_packages: Set[str] = set()
92
+ self._failed_packages: Set[str] = set()
93
+
94
+ # From APackageCache
95
+ self._cache: Dict[str, Any] = {}
96
+
97
+ # From APackageHelper
98
+ self._uninstalled_packages: Set[str] = set()
99
+
100
+ # Common
101
+ self._lock = threading.RLock()
102
+
103
+ # ========================================================================
104
+ # Package Discovery Methods (from APackageDiscovery)
105
+ # ========================================================================
106
+
107
+ def _find_project_root(self) -> Path:
108
+ """Find the project root directory by looking for markers."""
109
+ current = Path(__file__).parent.parent.parent
110
+ while current != current.parent:
111
+ if (current / 'pyproject.toml').exists() or (current / 'setup.py').exists():
112
+ return current
113
+ current = current.parent
114
+ return Path.cwd()
115
+
116
+ def discover_all_dependencies(self) -> Dict[str, str]:
117
+ """
118
+ Template method: Discover all dependencies from all sources.
119
+
120
+ Workflow:
121
+ 1. Check if cache is valid
122
+ 2. If not, discover from sources
123
+ 3. Add common mappings
124
+ 4. Update cache
125
+ 5. Return dependencies
126
+
127
+ Returns:
128
+ Dict mapping import_name -> package_name
129
+ """
130
+ # Return cached result if still valid
131
+ if self._is_cache_valid():
132
+ return self._cached_dependencies.copy()
133
+
134
+ # Cache invalid - rediscover
135
+ self.discovered_dependencies.clear()
136
+ self._discovery_sources.clear()
137
+
138
+ # Discover from all sources (abstract method)
139
+ self._discover_from_sources()
140
+
141
+ # Add common mappings
142
+ self._add_common_mappings()
143
+
144
+ # Convert to simple dict format and cache
145
+ result = {}
146
+ for import_name, dep_info in self.discovered_dependencies.items():
147
+ result[import_name] = dep_info.package_name
148
+
149
+ # Update cache
150
+ self._cached_dependencies = result.copy()
151
+ self._cache_valid = True
152
+ self._update_file_mtimes()
153
+
154
+ return result
155
+
156
+ @abstractmethod
157
+ def _discover_from_sources(self) -> None:
158
+ """
159
+ Discover dependencies from all sources (abstract step).
160
+
161
+ Implementations should discover from:
162
+ - pyproject.toml
163
+ - requirements.txt
164
+ - setup.py
165
+ - custom config files
166
+ """
167
+ pass
168
+
169
+ @abstractmethod
170
+ def _is_cache_valid(self) -> bool:
171
+ """
172
+ Check if cached dependencies are still valid (abstract step).
173
+
174
+ Returns:
175
+ True if cache is valid, False otherwise
176
+ """
177
+ pass
178
+
179
+ @abstractmethod
180
+ def _add_common_mappings(self) -> None:
181
+ """Add common import -> package mappings (abstract step)."""
182
+ pass
183
+
184
+ @abstractmethod
185
+ def _update_file_mtimes(self) -> None:
186
+ """Update file modification times for cache validation (abstract step)."""
187
+ pass
188
+
189
+ def get_discovery_sources(self) -> List[str]:
190
+ """Get list of sources used for discovery."""
191
+ return self._discovery_sources.copy()
192
+
193
+ # ========================================================================
194
+ # Package Installation Methods (from APackageInstaller)
195
+ # ========================================================================
196
+
197
+ def get_package_name(self) -> str:
198
+ """Get the package name this instance is for."""
199
+ return self._package_name
200
+
201
+ def set_mode(self, mode: LazyInstallMode) -> None:
202
+ """Set the installation mode."""
203
+ with self._lock:
204
+ self._mode = mode
205
+
206
+ def get_mode(self) -> LazyInstallMode:
207
+ """Get the current installation mode."""
208
+ return self._mode
209
+
210
+ def enable(self) -> None:
211
+ """Enable lazy installation."""
212
+ with self._lock:
213
+ self._enabled = True
214
+
215
+ def disable(self) -> None:
216
+ """Disable lazy installation."""
217
+ with self._lock:
218
+ self._enabled = False
219
+
220
+ def is_enabled(self) -> bool:
221
+ """Check if lazy installation is enabled."""
222
+ return self._enabled
223
+
224
+ @abstractmethod
225
+ def install_package(self, package_name: str, module_name: Optional[str] = None) -> bool:
226
+ """
227
+ Install a package (abstract method).
228
+
229
+ Args:
230
+ package_name: Name of package to install
231
+ module_name: Name of module being imported (for interactive mode)
232
+
233
+ Returns:
234
+ True if installation successful, False otherwise
235
+ """
236
+ pass
237
+
238
+ @abstractmethod
239
+ def _check_security_policy(self, package_name: str) -> Tuple[bool, str]:
240
+ """
241
+ Check security policy for package (abstract method).
242
+
243
+ Args:
244
+ package_name: Package to check
245
+
246
+ Returns:
247
+ Tuple of (allowed: bool, reason: str)
248
+ """
249
+ pass
250
+
251
+ @abstractmethod
252
+ def _run_pip_install(self, package_name: str, args: List[str]) -> bool:
253
+ """
254
+ Run pip install with arguments (abstract method).
255
+
256
+ Args:
257
+ package_name: Package to install
258
+ args: Additional pip arguments
259
+
260
+ Returns:
261
+ True if successful, False otherwise
262
+ """
263
+ pass
264
+
265
+ def get_stats(self) -> Dict[str, Any]:
266
+ """Get installation statistics."""
267
+ with self._lock:
268
+ return {
269
+ 'enabled': self._enabled,
270
+ 'mode': self._mode.value,
271
+ 'package_name': self._package_name,
272
+ 'installed_packages': list(self._installed_packages),
273
+ 'failed_packages': list(self._failed_packages),
274
+ 'total_installed': len(self._installed_packages),
275
+ 'total_failed': len(self._failed_packages)
276
+ }
277
+
278
+ # ========================================================================
279
+ # Package Caching Methods (from APackageCache)
280
+ # ========================================================================
281
+
282
+ def get_cached(self, key: str) -> Optional[Any]:
283
+ """
284
+ Get cached value (abstract method).
285
+
286
+ Args:
287
+ key: Cache key
288
+
289
+ Returns:
290
+ Cached value or None if not found
291
+ """
292
+ with self._lock:
293
+ return self._cache.get(key)
294
+
295
+ def set_cached(self, key: str, value: Any) -> None:
296
+ """
297
+ Set cached value (abstract method).
298
+
299
+ Args:
300
+ key: Cache key
301
+ value: Value to cache
302
+ """
303
+ with self._lock:
304
+ self._cache[key] = value
305
+
306
+ def clear_cache(self) -> None:
307
+ """Clear all cached values."""
308
+ with self._lock:
309
+ self._cache.clear()
310
+
311
+ @abstractmethod
312
+ def is_cache_valid(self, key: str) -> bool:
313
+ """
314
+ Check if cache entry is still valid (abstract method).
315
+
316
+ Args:
317
+ key: Cache key
318
+
319
+ Returns:
320
+ True if valid, False otherwise
321
+ """
322
+ pass
323
+
324
+ # ========================================================================
325
+ # Package Helper Methods (from APackageHelper)
326
+ # ========================================================================
327
+
328
+ def installed(self, package_name: str) -> bool:
329
+ """
330
+ Check if a package is installed.
331
+
332
+ Uses cache first to avoid expensive operations.
333
+ Checks persistent cache, then in-memory cache, then importability.
334
+
335
+ Args:
336
+ package_name: Package name to check (e.g., 'pymongo', 'msgpack')
337
+
338
+ Returns:
339
+ True if package is installed, False otherwise
340
+ """
341
+ # Check in-memory cache first (fast)
342
+ with self._lock:
343
+ if package_name in self._installed_packages:
344
+ return True
345
+ if package_name in self._uninstalled_packages:
346
+ return False
347
+
348
+ # Check persistent cache (abstract method)
349
+ if self._check_persistent_cache(package_name):
350
+ with self._lock:
351
+ self._installed_packages.add(package_name)
352
+ self._uninstalled_packages.discard(package_name)
353
+ return True
354
+
355
+ # Check actual installation (expensive) - abstract method
356
+ is_installed = self._check_importability(package_name)
357
+
358
+ # Update caches
359
+ with self._lock:
360
+ if is_installed:
361
+ self._installed_packages.add(package_name)
362
+ self._uninstalled_packages.discard(package_name)
363
+ self._mark_installed_in_persistent_cache(package_name)
364
+ else:
365
+ self._uninstalled_packages.add(package_name)
366
+ self._installed_packages.discard(package_name)
367
+
368
+ return is_installed
369
+
370
+ def uninstalled(self, package_name: str) -> bool:
371
+ """
372
+ Check if a package is uninstalled.
373
+
374
+ Uses cache first to avoid expensive operations.
375
+
376
+ Args:
377
+ package_name: Package name to check (e.g., 'pymongo', 'msgpack')
378
+
379
+ Returns:
380
+ True if package is uninstalled, False otherwise
381
+ """
382
+ return not self.installed(package_name)
383
+
384
+ def install(self, *package_names: str) -> None:
385
+ """
386
+ Install one or more packages using pip.
387
+
388
+ Skips packages that are already installed (using cache).
389
+ Only installs unique packages to avoid duplicate operations.
390
+ Updates cache after successful installation.
391
+
392
+ Args:
393
+ *package_names: One or more package names to install (e.g., 'pymongo', 'msgpack')
394
+
395
+ Raises:
396
+ subprocess.CalledProcessError: If installation fails
397
+ """
398
+ if not package_names:
399
+ return
400
+
401
+ # Get unique packages only (preserves order while removing duplicates)
402
+ unique_names = list(dict.fromkeys(package_names))
403
+
404
+ # Filter out packages that are already installed (check cache first)
405
+ to_install = []
406
+ with self._lock:
407
+ for name in unique_names:
408
+ if name not in self._installed_packages:
409
+ # Double-check if not in cache
410
+ if not self.installed(name):
411
+ to_install.append(name)
412
+
413
+ if not to_install:
414
+ # All packages already installed
415
+ return
416
+
417
+ # Install packages (abstract method)
418
+ self._run_install(*to_install)
419
+
420
+ # Update cache after successful installation
421
+ with self._lock:
422
+ for name in to_install:
423
+ self._installed_packages.add(name)
424
+ self._uninstalled_packages.discard(name)
425
+ self._mark_installed_in_persistent_cache(name)
426
+
427
+ def uninstall(self, *package_names: str) -> None:
428
+ """
429
+ Uninstall one or more packages using pip.
430
+
431
+ Skips packages that are already uninstalled (using cache).
432
+ Only uninstalls unique packages to avoid duplicate operations.
433
+ Updates cache after successful uninstallation.
434
+
435
+ Args:
436
+ *package_names: One or more package names to uninstall (e.g., 'pymongo', 'msgpack')
437
+
438
+ Raises:
439
+ subprocess.CalledProcessError: If uninstallation fails
440
+ """
441
+ if not package_names:
442
+ return
443
+
444
+ # Get unique packages only (preserves order while removing duplicates)
445
+ unique_names = list(dict.fromkeys(package_names))
446
+
447
+ # Filter out packages that are already uninstalled (check cache first)
448
+ to_uninstall = []
449
+ with self._lock:
450
+ for name in unique_names:
451
+ if name not in self._uninstalled_packages:
452
+ # Double-check if not uninstalled
453
+ if self.installed(name):
454
+ to_uninstall.append(name)
455
+
456
+ if not to_uninstall:
457
+ # All packages already uninstalled
458
+ return
459
+
460
+ # Uninstall packages (abstract method)
461
+ self._run_uninstall(*to_uninstall)
462
+
463
+ # Update cache after successful uninstallation
464
+ with self._lock:
465
+ for name in to_uninstall:
466
+ self._uninstalled_packages.add(name)
467
+ self._installed_packages.discard(name)
468
+ self._mark_uninstalled_in_persistent_cache(name)
469
+
470
+ @abstractmethod
471
+ def _check_importability(self, package_name: str) -> bool:
472
+ """
473
+ Check if package is importable (abstract method).
474
+
475
+ Concrete implementations should use importlib.util.find_spec or similar.
476
+
477
+ Args:
478
+ package_name: Package name to check
479
+
480
+ Returns:
481
+ True if importable, False otherwise
482
+ """
483
+ pass
484
+
485
+ @abstractmethod
486
+ def _check_persistent_cache(self, package_name: str) -> bool:
487
+ """
488
+ Check persistent cache for package installation status (abstract method).
489
+
490
+ Args:
491
+ package_name: Package name to check
492
+
493
+ Returns:
494
+ True if found in persistent cache as installed, False otherwise
495
+ """
496
+ pass
497
+
498
+ @abstractmethod
499
+ def _mark_installed_in_persistent_cache(self, package_name: str) -> None:
500
+ """
501
+ Mark package as installed in persistent cache (abstract method).
502
+
503
+ Args:
504
+ package_name: Package name to mark
505
+ """
506
+ pass
507
+
508
+ @abstractmethod
509
+ def _mark_uninstalled_in_persistent_cache(self, package_name: str) -> None:
510
+ """
511
+ Mark package as uninstalled in persistent cache (abstract method).
512
+
513
+ Args:
514
+ package_name: Package name to mark
515
+ """
516
+ pass
517
+
518
+ @abstractmethod
519
+ def _run_install(self, *package_names: str) -> None:
520
+ """
521
+ Run pip install for packages (abstract method).
522
+
523
+ Args:
524
+ *package_names: Package names to install
525
+
526
+ Raises:
527
+ subprocess.CalledProcessError: If installation fails
528
+ """
529
+ pass
530
+
531
+ @abstractmethod
532
+ def _run_uninstall(self, *package_names: str) -> None:
533
+ """
534
+ Run pip uninstall for packages (abstract method).
535
+
536
+ Args:
537
+ *package_names: Package names to uninstall
538
+
539
+ Raises:
540
+ subprocess.CalledProcessError: If uninstallation fails
541
+ """
542
+ pass
543
+
544
+ # ========================================================================
545
+ # IPackageHelper Interface Methods (stubs - to be implemented by subclasses)
546
+ # ========================================================================
547
+
548
+ # Note: Many methods from IPackageHelper are already implemented above.
549
+ # The following are stubs that need concrete implementations:
550
+
551
+ def install_and_import(self, module_name: str, package_name: Optional[str] = None) -> Tuple[Optional[ModuleType], bool]:
552
+ """Install package and import module (from IPackageInstaller)."""
553
+ raise NotImplementedError("Subclasses must implement install_and_import")
554
+
555
+ def get_package_for_import(self, import_name: str) -> Optional[str]:
556
+ """Get package name for a given import name (from IPackageDiscovery)."""
557
+ raise NotImplementedError("Subclasses must implement get_package_for_import")
558
+
559
+ def get_imports_for_package(self, package_name: str) -> List[str]:
560
+ """Get all possible import names for a package (from IPackageDiscovery)."""
561
+ raise NotImplementedError("Subclasses must implement get_imports_for_package")
562
+
563
+ def get_package_name(self, import_name: str) -> Optional[str]:
564
+ """Get package name for an import name (from IDependencyMapper)."""
565
+ raise NotImplementedError("Subclasses must implement get_package_name")
566
+
567
+ def get_import_names(self, package_name: str) -> List[str]:
568
+ """Get all import names for a package (from IDependencyMapper)."""
569
+ raise NotImplementedError("Subclasses must implement get_import_names")
570
+
571
+ def is_stdlib_or_builtin(self, import_name: str) -> bool:
572
+ """Check if import name is stdlib or builtin (from IDependencyMapper)."""
573
+ raise NotImplementedError("Subclasses must implement is_stdlib_or_builtin")
574
+
575
+ # Note: is_enabled(package_name) from IConfigManager is removed to avoid conflict
576
+ # with is_enabled() instance method. Use LazyInstallConfig.is_enabled(package_name) instead.
577
+
578
+ def get_mode(self, package_name: str) -> str:
579
+ """Get installation mode for a package (from IConfigManager)."""
580
+ raise NotImplementedError("Subclasses must implement get_mode")
581
+
582
+ def get_load_mode(self, package_name: str) -> Any:
583
+ """Get load mode for a package (from IConfigManager)."""
584
+ raise NotImplementedError("Subclasses must implement get_load_mode")
585
+
586
+ def get_install_mode(self, package_name: str) -> Any:
587
+ """Get install mode for a package (from IConfigManager)."""
588
+ raise NotImplementedError("Subclasses must implement get_install_mode")
589
+
590
+ def get_mode_config(self, package_name: str) -> Optional[Any]:
591
+ """Get full mode configuration for a package (from IConfigManager)."""
592
+ raise NotImplementedError("Subclasses must implement get_mode_config")
593
+
594
+ def get_manifest_signature(self, package_name: str) -> Optional[Tuple[str, float, float]]:
595
+ """Get manifest file signature (from IManifestLoader)."""
596
+ raise NotImplementedError("Subclasses must implement get_manifest_signature")
597
+
598
+ def get_shared_dependencies(self, package_name: str, signature: Optional[Tuple[str, float, float]] = None) -> Dict[str, str]:
599
+ """Get shared dependencies from manifest (from IManifestLoader)."""
600
+ raise NotImplementedError("Subclasses must implement get_shared_dependencies")
601
+
602
+ def get_watched_prefixes(self, package_name: str) -> Tuple[str, ...]:
603
+ """Get watched prefixes from manifest (from IManifestLoader)."""
604
+ raise NotImplementedError("Subclasses must implement get_watched_prefixes")
605
+
606
+ # =============================================================================
607
+ # DEPRECATED CLASSES (for backward compatibility)
608
+ # =============================================================================
609
+
610
+ # =============================================================================
611
+ # ABSTRACT PACKAGE HELPER STRATEGY
612
+ # =============================================================================
613
+
614
+ class APackageHelperStrategy(IPackageHelperStrategy, ABC):
615
+ """
616
+ Abstract base class for package helper strategies.
617
+
618
+ Operations on a single package (installing, uninstalling, checking).
619
+ All package helper strategies must extend this class.
620
+ """
621
+
622
+ @abstractmethod
623
+ def install(self, package_name: str) -> bool:
624
+ """Install the package."""
625
+ ...
626
+
627
+ @abstractmethod
628
+ def uninstall(self, package_name: str) -> None:
629
+ """Uninstall the package."""
630
+ ...
631
+
632
+ @abstractmethod
633
+ def check_installed(self, name: str) -> bool:
634
+ """Check if package is installed."""
635
+ ...
636
+
637
+ @abstractmethod
638
+ def get_version(self, name: str) -> Optional[str]:
639
+ """Get installed version."""
640
+ ...
641
+
642
+ # =============================================================================
643
+ # ABSTRACT PACKAGE MANAGER STRATEGY
644
+ # =============================================================================
645
+
646
+ class APackageManagerStrategy(IPackageManagerStrategy, ABC):
647
+ """
648
+ Abstract base class for package manager strategies.
649
+
650
+ Orchestrates multiple packages (installation, discovery, policy).
651
+ All package manager strategies must extend this class.
652
+ """
653
+
654
+ @abstractmethod
655
+ def install_package(self, package_name: str, module_name: Optional[str] = None) -> bool:
656
+ """Install a package."""
657
+ ...
658
+
659
+ @abstractmethod
660
+ def uninstall_package(self, package_name: str) -> None:
661
+ """Uninstall a package."""
662
+ ...
663
+
664
+ @abstractmethod
665
+ def discover_dependencies(self) -> Dict[str, str]:
666
+ """Discover dependencies."""
667
+ ...
668
+
669
+ @abstractmethod
670
+ def check_security_policy(self, package_name: str) -> Tuple[bool, str]:
671
+ """Check security policy."""
672
+ ...
673
+
674
+ # =============================================================================
675
+ # ABSTRACT INSTALLATION EXECUTION STRATEGY
676
+ # =============================================================================
677
+
678
+ class AInstallExecutionStrategy(IInstallExecutionStrategy, ABC):
679
+ """
680
+ Abstract base class for installation execution strategies.
681
+
682
+ HOW to execute installation (pip, wheel, cached, async).
683
+ """
684
+
685
+ @abstractmethod
686
+ def execute_install(self, package_name: str, policy_args: List[str]) -> Any:
687
+ """Execute installation of a package."""
688
+ ...
689
+
690
+ @abstractmethod
691
+ def execute_uninstall(self, package_name: str) -> bool:
692
+ """Execute uninstallation of a package."""
693
+ ...
694
+
695
+ # =============================================================================
696
+ # ABSTRACT INSTALLATION TIMING STRATEGY
697
+ # =============================================================================
698
+
699
+ class AInstallTimingStrategy(IInstallTimingStrategy, ABC):
700
+ """
701
+ Abstract base class for installation timing strategies.
702
+
703
+ WHEN to install packages (on-demand, upfront, temporary, etc.).
704
+ """
705
+
706
+ @abstractmethod
707
+ def should_install_now(self, package_name: str, context: Any) -> bool:
708
+ """Determine if package should be installed now."""
709
+ ...
710
+
711
+ @abstractmethod
712
+ def should_uninstall_after(self, package_name: str, context: Any) -> bool:
713
+ """Determine if package should be uninstalled after use."""
714
+ ...
715
+
716
+ @abstractmethod
717
+ def get_install_priority(self, packages: List[str]) -> List[str]:
718
+ """Get priority order for installing packages."""
719
+ ...
720
+
721
+ # =============================================================================
722
+ # ABSTRACT DISCOVERY STRATEGY
723
+ # =============================================================================
724
+
725
+ class ADiscoveryStrategy(IDiscoveryStrategy, ABC):
726
+ """
727
+ Abstract base class for discovery strategies.
728
+
729
+ HOW to discover dependencies (from files, manifest, auto-detect).
730
+ """
731
+
732
+ @abstractmethod
733
+ def discover(self, project_root: Any) -> Dict[str, str]:
734
+ """Discover dependencies from sources."""
735
+ ...
736
+
737
+ @abstractmethod
738
+ def get_source(self, import_name: str) -> Optional[str]:
739
+ """Get the source of a discovered dependency."""
740
+ ...
741
+
742
+ # =============================================================================
743
+ # ABSTRACT POLICY STRATEGY
744
+ # =============================================================================
745
+
746
+ class APolicyStrategy(IPolicyStrategy, ABC):
747
+ """
748
+ Abstract base class for policy strategies.
749
+
750
+ WHAT can be installed (security/policy enforcement).
751
+ """
752
+
753
+ @abstractmethod
754
+ def is_allowed(self, package_name: str) -> Tuple[bool, str]:
755
+ """Check if package is allowed to be installed."""
756
+ ...
757
+
758
+ @abstractmethod
759
+ def get_pip_args(self, package_name: str) -> List[str]:
760
+ """Get pip arguments based on policy."""
761
+ ...
762
+
763
+ # =============================================================================
764
+ # ABSTRACT MAPPING STRATEGY
765
+ # =============================================================================
766
+
767
+ class AMappingStrategy(IMappingStrategy, ABC):
768
+ """
769
+ Abstract base class for mapping strategies.
770
+
771
+ HOW to map import names to package names.
772
+ """
773
+
774
+ @abstractmethod
775
+ def map_import_to_package(self, import_name: str) -> Optional[str]:
776
+ """Map import name to package name."""
777
+ ...
778
+
779
+ @abstractmethod
780
+ def map_package_to_imports(self, package_name: str) -> List[str]:
781
+ """Map package name to possible import names."""
782
+ ...
783
+
784
+ # =============================================================================
785
+ # EXPORT ALL
786
+ # =============================================================================
787
+
788
+ __all__ = [
789
+ 'APackageHelper',
790
+ 'APackageHelperStrategy',
791
+ 'APackageManagerStrategy',
792
+ 'AInstallExecutionStrategy',
793
+ 'AInstallTimingStrategy',
794
+ 'ADiscoveryStrategy',
795
+ 'APolicyStrategy',
796
+ 'AMappingStrategy',
797
+ ]
798
+