exonware-xwlazy 0.1.0.11__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.
- exonware/__init__.py +22 -0
- exonware/xwlazy/__init__.py +0 -0
- exonware/xwlazy/common/__init__.py +47 -0
- exonware/xwlazy/common/base.py +58 -0
- exonware/xwlazy/common/cache.py +506 -0
- exonware/xwlazy/common/logger.py +268 -0
- exonware/xwlazy/common/services/__init__.py +72 -0
- exonware/xwlazy/common/services/dependency_mapper.py +234 -0
- exonware/xwlazy/common/services/install_async_utils.py +169 -0
- exonware/xwlazy/common/services/install_cache_utils.py +257 -0
- exonware/xwlazy/common/services/keyword_detection.py +292 -0
- exonware/xwlazy/common/services/spec_cache.py +173 -0
- exonware/xwlazy/common/strategies/__init__.py +28 -0
- exonware/xwlazy/common/strategies/caching_dict.py +45 -0
- exonware/xwlazy/common/strategies/caching_installation.py +89 -0
- exonware/xwlazy/common/strategies/caching_lfu.py +67 -0
- exonware/xwlazy/common/strategies/caching_lru.py +64 -0
- exonware/xwlazy/common/strategies/caching_multitier.py +60 -0
- exonware/xwlazy/common/strategies/caching_ttl.py +60 -0
- {xwlazy/lazy → exonware/xwlazy}/config.py +52 -20
- exonware/xwlazy/contracts.py +1410 -0
- exonware/xwlazy/defs.py +397 -0
- xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -8
- exonware/xwlazy/facade.py +1049 -0
- exonware/xwlazy/module/__init__.py +18 -0
- exonware/xwlazy/module/base.py +569 -0
- exonware/xwlazy/module/data.py +17 -0
- exonware/xwlazy/module/facade.py +247 -0
- exonware/xwlazy/module/importer_engine.py +2161 -0
- exonware/xwlazy/module/strategies/__init__.py +22 -0
- exonware/xwlazy/module/strategies/module_helper_lazy.py +94 -0
- exonware/xwlazy/module/strategies/module_helper_simple.py +66 -0
- exonware/xwlazy/module/strategies/module_manager_advanced.py +112 -0
- exonware/xwlazy/module/strategies/module_manager_simple.py +96 -0
- exonware/xwlazy/package/__init__.py +18 -0
- exonware/xwlazy/package/base.py +807 -0
- xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +62 -10
- exonware/xwlazy/package/data.py +17 -0
- exonware/xwlazy/package/facade.py +481 -0
- exonware/xwlazy/package/services/__init__.py +84 -0
- exonware/xwlazy/package/services/async_install_handle.py +89 -0
- exonware/xwlazy/package/services/config_manager.py +246 -0
- exonware/xwlazy/package/services/discovery.py +374 -0
- {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -16
- exonware/xwlazy/package/services/install_async.py +278 -0
- exonware/xwlazy/package/services/install_cache.py +146 -0
- exonware/xwlazy/package/services/install_interactive.py +60 -0
- exonware/xwlazy/package/services/install_policy.py +158 -0
- exonware/xwlazy/package/services/install_registry.py +56 -0
- exonware/xwlazy/package/services/install_result.py +17 -0
- exonware/xwlazy/package/services/install_sbom.py +154 -0
- exonware/xwlazy/package/services/install_utils.py +83 -0
- exonware/xwlazy/package/services/installer_engine.py +408 -0
- exonware/xwlazy/package/services/lazy_installer.py +720 -0
- {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +42 -25
- exonware/xwlazy/package/services/strategy_registry.py +188 -0
- exonware/xwlazy/package/strategies/__init__.py +57 -0
- exonware/xwlazy/package/strategies/package_discovery_file.py +130 -0
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +85 -0
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +102 -0
- exonware/xwlazy/package/strategies/package_execution_async.py +114 -0
- exonware/xwlazy/package/strategies/package_execution_cached.py +91 -0
- exonware/xwlazy/package/strategies/package_execution_pip.py +100 -0
- exonware/xwlazy/package/strategies/package_execution_wheel.py +107 -0
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +101 -0
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +106 -0
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +101 -0
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +58 -0
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +58 -0
- exonware/xwlazy/package/strategies/package_policy_permissive.py +47 -0
- exonware/xwlazy/package/strategies/package_timing_clean.py +68 -0
- exonware/xwlazy/package/strategies/package_timing_full.py +67 -0
- exonware/xwlazy/package/strategies/package_timing_smart.py +69 -0
- exonware/xwlazy/package/strategies/package_timing_temporary.py +67 -0
- exonware/xwlazy/runtime/__init__.py +18 -0
- exonware/xwlazy/runtime/adaptive_learner.py +131 -0
- exonware/xwlazy/runtime/base.py +276 -0
- exonware/xwlazy/runtime/facade.py +95 -0
- exonware/xwlazy/runtime/intelligent_selector.py +173 -0
- exonware/xwlazy/runtime/metrics.py +64 -0
- exonware/xwlazy/runtime/performance.py +39 -0
- exonware/xwlazy/version.py +2 -2
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/METADATA +86 -10
- exonware_xwlazy-0.1.0.19.dist-info/RECORD +87 -0
- exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
- xwlazy/__init__.py +0 -34
- xwlazy/lazy/__init__.py +0 -301
- xwlazy/lazy/bootstrap.py +0 -106
- xwlazy/lazy/lazy_base.py +0 -465
- xwlazy/lazy/lazy_contracts.py +0 -290
- xwlazy/lazy/lazy_core.py +0 -3727
- xwlazy/lazy/logging_utils.py +0 -194
- xwlazy/version.py +0 -77
- /xwlazy/lazy/lazy_state.py → /exonware/xwlazy/common/services/state_manager.py +0 -0
- {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.11.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
|
+
|