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