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,169 @@
1
+ """
2
+ Installation Async Utilities
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+ Version: 0.1.0.19
8
+ Generation Date: 15-Nov-2025
9
+
10
+ Shared utilities for async installation operations.
11
+ Used by both execution strategies and LazyInstaller.
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import json
17
+ import asyncio
18
+ import subprocess
19
+ from typing import Optional
20
+
21
+ # Lazy imports
22
+ def _get_logger():
23
+ """Get logger (lazy import to avoid circular dependency)."""
24
+ from ..logger import get_logger
25
+ return get_logger("xwlazy.install_async_utils")
26
+
27
+ logger = None
28
+
29
+ def _ensure_logging_initialized():
30
+ """Ensure logging utilities are initialized."""
31
+ global logger
32
+ if logger is None:
33
+ logger = _get_logger()
34
+
35
+
36
+ async def get_package_size_mb(package_name: str) -> Optional[float]:
37
+ """
38
+ Get package size in MB by checking pip show or download size.
39
+
40
+ Args:
41
+ package_name: Package name to check
42
+
43
+ Returns:
44
+ Size in MB or None if cannot determine
45
+ """
46
+ try:
47
+ process = await asyncio.create_subprocess_exec(
48
+ sys.executable, '-m', 'pip', 'show', package_name,
49
+ stdout=asyncio.subprocess.PIPE,
50
+ stderr=asyncio.subprocess.PIPE
51
+ )
52
+ stdout, _ = await process.communicate()
53
+
54
+ if process.returncode == 0:
55
+ output = stdout.decode()
56
+ for line in output.split('\n'):
57
+ if line.startswith('Location:'):
58
+ location = line.split(':', 1)[1].strip()
59
+ try:
60
+ total_size = 0
61
+ for dirpath, dirnames, filenames in os.walk(location):
62
+ for filename in filenames:
63
+ filepath = os.path.join(dirpath, filename)
64
+ if os.path.exists(filepath):
65
+ total_size += os.path.getsize(filepath)
66
+ return total_size / (1024 * 1024)
67
+ except Exception:
68
+ pass
69
+ except Exception:
70
+ pass
71
+
72
+ # Fallback: Try to get download size from PyPI
73
+ try:
74
+ import urllib.request
75
+ url = f"https://pypi.org/pypi/{package_name}/json"
76
+ with urllib.request.urlopen(url, timeout=5) as response:
77
+ data = json.loads(response.read())
78
+ if 'urls' in data and data['urls']:
79
+ latest = data['urls'][0]
80
+ if 'size' in latest:
81
+ return latest['size'] / (1024 * 1024)
82
+ except Exception:
83
+ pass
84
+
85
+ return None
86
+
87
+
88
+ async def async_install_package(
89
+ package_name: str,
90
+ policy_args: Optional[list] = None
91
+ ) -> tuple[bool, Optional[str]]:
92
+ """
93
+ Install a package asynchronously using asyncio subprocess.
94
+
95
+ Args:
96
+ package_name: Package name to install
97
+ policy_args: Optional policy arguments (index URLs, trusted hosts, etc.)
98
+
99
+ Returns:
100
+ Tuple of (success: bool, error_message: Optional[str])
101
+ """
102
+ _ensure_logging_initialized()
103
+ try:
104
+ pip_args = [sys.executable, '-m', 'pip', 'install']
105
+ if policy_args:
106
+ pip_args.extend(policy_args)
107
+ pip_args.append(package_name)
108
+
109
+ process = await asyncio.create_subprocess_exec(
110
+ *pip_args,
111
+ stdout=asyncio.subprocess.PIPE,
112
+ stderr=asyncio.subprocess.PIPE
113
+ )
114
+
115
+ stdout, stderr = await process.communicate()
116
+
117
+ if process.returncode == 0:
118
+ return True, None
119
+ else:
120
+ error_msg = stderr.decode() if stderr else "Unknown error"
121
+ logger.error(f"Failed to install {package_name}: {error_msg}")
122
+ return False, error_msg
123
+ except Exception as e:
124
+ logger.error(f"Error in async install of {package_name}: {e}")
125
+ return False, str(e)
126
+
127
+
128
+ async def async_uninstall_package(
129
+ package_name: str,
130
+ quiet: bool = True
131
+ ) -> bool:
132
+ """
133
+ Uninstall a package asynchronously.
134
+
135
+ Args:
136
+ package_name: Package name to uninstall
137
+ quiet: If True, suppress output
138
+
139
+ Returns:
140
+ True if successful, False otherwise
141
+ """
142
+ _ensure_logging_initialized()
143
+ try:
144
+ pip_args = [sys.executable, '-m', 'pip', 'uninstall', '-y', package_name]
145
+
146
+ process = await asyncio.create_subprocess_exec(
147
+ *pip_args,
148
+ stdout=asyncio.subprocess.PIPE if quiet else None,
149
+ stderr=asyncio.subprocess.PIPE if quiet else None
150
+ )
151
+
152
+ await process.communicate()
153
+
154
+ if process.returncode == 0:
155
+ if not quiet:
156
+ logger.info(f"Uninstalled {package_name}")
157
+ return True
158
+ return False
159
+ except Exception as e:
160
+ logger.debug(f"Failed to uninstall {package_name}: {e}")
161
+ return False
162
+
163
+
164
+ __all__ = [
165
+ 'get_package_size_mb',
166
+ 'async_install_package',
167
+ 'async_uninstall_package',
168
+ ]
169
+
@@ -0,0 +1,257 @@
1
+ """
2
+ Installation Cache Utilities
3
+
4
+ Company: eXonware.com
5
+ Author: Eng. Muhammad AlShehri
6
+ Email: connect@exonware.com
7
+ Version: 0.1.0.19
8
+ Generation Date: 15-Nov-2025
9
+
10
+ Shared utilities for cache management (wheels, install trees).
11
+ Used by both execution strategies and LazyInstaller.
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import shutil
17
+ import sysconfig
18
+ import tempfile
19
+ import subprocess
20
+ import zipfile
21
+ from pathlib import Path
22
+ from typing import Optional, List
23
+ from contextlib import suppress
24
+
25
+ # Lazy imports
26
+ def _get_logger():
27
+ """Get logger (lazy import to avoid circular dependency)."""
28
+ from ..logger import get_logger
29
+ return get_logger("xwlazy.install_cache_utils")
30
+
31
+ logger = None
32
+
33
+ # Environment variables
34
+ _DEFAULT_ASYNC_CACHE_DIR = Path(
35
+ os.environ.get(
36
+ "XWLAZY_ASYNC_CACHE_DIR",
37
+ os.path.join(os.path.expanduser("~"), ".xwlazy", "wheel-cache"),
38
+ )
39
+ )
40
+
41
+ def _ensure_logging_initialized():
42
+ """Ensure logging utilities are initialized."""
43
+ global logger
44
+ if logger is None:
45
+ logger = _get_logger()
46
+
47
+
48
+ def get_default_cache_dir() -> Path:
49
+ """Get the default cache directory."""
50
+ return _DEFAULT_ASYNC_CACHE_DIR
51
+
52
+
53
+ def get_cache_dir(cache_dir: Optional[Path] = None) -> Path:
54
+ """Get cache directory, creating it if necessary."""
55
+ if cache_dir is None:
56
+ cache_dir = _DEFAULT_ASYNC_CACHE_DIR
57
+ path = Path(cache_dir).expanduser()
58
+ path.mkdir(parents=True, exist_ok=True)
59
+ return path
60
+
61
+
62
+ def get_wheel_path(package_name: str, cache_dir: Optional[Path] = None) -> Path:
63
+ """Get the cached wheel file path for a package."""
64
+ cache = get_cache_dir(cache_dir)
65
+ safe = package_name.replace("/", "_").replace("\\", "_").replace(":", "_")
66
+ return cache / f"{safe}.whl"
67
+
68
+
69
+ def get_install_tree_dir(package_name: str, cache_dir: Optional[Path] = None) -> Path:
70
+ """Get the cached install directory for a package."""
71
+ cache = get_cache_dir(cache_dir)
72
+ safe = package_name.replace("/", "_").replace("\\", "_").replace(":", "_")
73
+ return cache / "installs" / safe
74
+
75
+
76
+ def get_site_packages_dir() -> Path:
77
+ """Get the site-packages directory."""
78
+ purelib = sysconfig.get_paths().get("purelib")
79
+ if not purelib:
80
+ purelib = sysconfig.get_paths().get("platlib", sys.prefix)
81
+ path = Path(purelib)
82
+ path.mkdir(parents=True, exist_ok=True)
83
+ return path
84
+
85
+
86
+ def pip_install_from_path(wheel_path: Path, policy_args: Optional[List[str]] = None) -> bool:
87
+ """Install a wheel file using pip."""
88
+ try:
89
+ pip_args = [
90
+ sys.executable,
91
+ '-m',
92
+ 'pip',
93
+ 'install',
94
+ '--no-deps',
95
+ '--no-input',
96
+ '--disable-pip-version-check',
97
+ ]
98
+ if policy_args:
99
+ pip_args.extend(policy_args)
100
+ pip_args.append(str(wheel_path))
101
+ result = subprocess.run(
102
+ pip_args,
103
+ capture_output=True,
104
+ text=True,
105
+ check=True,
106
+ )
107
+ return result.returncode == 0
108
+ except subprocess.CalledProcessError:
109
+ return False
110
+
111
+
112
+ def ensure_cached_wheel(
113
+ package_name: str,
114
+ policy_args: Optional[List[str]] = None,
115
+ cache_dir: Optional[Path] = None
116
+ ) -> Optional[Path]:
117
+ """Ensure a wheel is cached, downloading it if necessary."""
118
+ wheel_path = get_wheel_path(package_name, cache_dir)
119
+ if wheel_path.exists():
120
+ return wheel_path
121
+
122
+ cache = get_cache_dir(cache_dir)
123
+ try:
124
+ pip_args = [
125
+ sys.executable,
126
+ '-m',
127
+ 'pip',
128
+ 'wheel',
129
+ '--no-deps',
130
+ '--disable-pip-version-check',
131
+ ]
132
+ if policy_args:
133
+ pip_args.extend(policy_args)
134
+ pip_args.extend(['--wheel-dir', str(cache), package_name])
135
+ result = subprocess.run(
136
+ pip_args,
137
+ capture_output=True,
138
+ text=True,
139
+ check=True,
140
+ )
141
+ if result.returncode != 0:
142
+ return None
143
+ candidates = sorted(cache.glob("*.whl"), key=lambda p: p.stat().st_mtime, reverse=True)
144
+ if not candidates:
145
+ return None
146
+ primary = candidates[0]
147
+ if wheel_path.exists():
148
+ with suppress(Exception):
149
+ wheel_path.unlink()
150
+ primary.rename(wheel_path)
151
+ for leftover in candidates[1:]:
152
+ with suppress(Exception):
153
+ leftover.unlink()
154
+ return wheel_path
155
+ except subprocess.CalledProcessError:
156
+ return None
157
+
158
+
159
+ def install_from_cached_tree(
160
+ package_name: str,
161
+ cache_dir: Optional[Path] = None
162
+ ) -> bool:
163
+ """Install from a cached install tree."""
164
+ _ensure_logging_initialized()
165
+ src = get_install_tree_dir(package_name, cache_dir)
166
+ if not src.exists() or not any(src.iterdir()):
167
+ return False
168
+ target_root = get_site_packages_dir()
169
+ try:
170
+ for item in src.iterdir():
171
+ dest = target_root / item.name
172
+ if dest.exists():
173
+ if dest.is_dir():
174
+ shutil.rmtree(dest, ignore_errors=True)
175
+ else:
176
+ with suppress(FileNotFoundError):
177
+ dest.unlink()
178
+ if item.is_dir():
179
+ shutil.copytree(item, dest)
180
+ else:
181
+ dest.parent.mkdir(parents=True, exist_ok=True)
182
+ shutil.copy2(item, dest)
183
+ return True
184
+ except Exception as exc:
185
+ logger.debug("Cached tree install failed for %s: %s", package_name, exc)
186
+ return False
187
+
188
+
189
+ def materialize_cached_tree(
190
+ package_name: str,
191
+ wheel_path: Path,
192
+ cache_dir: Optional[Path] = None
193
+ ) -> None:
194
+ """Materialize a cached install tree from a wheel file."""
195
+ _ensure_logging_initialized()
196
+ if not wheel_path or not wheel_path.exists():
197
+ return
198
+ target_dir = get_install_tree_dir(package_name, cache_dir)
199
+ if target_dir.exists() and any(target_dir.iterdir()):
200
+ return
201
+ parent = target_dir.parent
202
+ parent.mkdir(parents=True, exist_ok=True)
203
+ temp_dir = Path(
204
+ tempfile.mkdtemp(prefix="xwlazy-cache-", dir=str(parent))
205
+ )
206
+ try:
207
+ with zipfile.ZipFile(wheel_path, "r") as archive:
208
+ archive.extractall(temp_dir)
209
+ if target_dir.exists():
210
+ shutil.rmtree(target_dir, ignore_errors=True)
211
+ shutil.move(str(temp_dir), str(target_dir))
212
+ except Exception as exc:
213
+ logger.debug("Failed to materialize cached tree for %s: %s", package_name, exc)
214
+ with suppress(Exception):
215
+ shutil.rmtree(temp_dir, ignore_errors=True)
216
+ else:
217
+ return
218
+ finally:
219
+ if temp_dir.exists():
220
+ shutil.rmtree(temp_dir, ignore_errors=True)
221
+
222
+
223
+ def has_cached_install_tree(
224
+ package_name: str,
225
+ cache_dir: Optional[Path] = None
226
+ ) -> bool:
227
+ """Check if a cached install tree exists."""
228
+ target = get_install_tree_dir(package_name, cache_dir)
229
+ return target.exists() and any(target.iterdir())
230
+
231
+
232
+ def install_from_cached_wheel(
233
+ package_name: str,
234
+ policy_args: Optional[List[str]] = None,
235
+ cache_dir: Optional[Path] = None
236
+ ) -> bool:
237
+ """Install from a cached wheel file."""
238
+ wheel_path = get_wheel_path(package_name, cache_dir)
239
+ if not wheel_path.exists():
240
+ return False
241
+ return pip_install_from_path(wheel_path, policy_args)
242
+
243
+
244
+ __all__ = [
245
+ 'get_default_cache_dir',
246
+ 'get_cache_dir',
247
+ 'get_wheel_path',
248
+ 'get_install_tree_dir',
249
+ 'get_site_packages_dir',
250
+ 'pip_install_from_path',
251
+ 'ensure_cached_wheel',
252
+ 'install_from_cached_tree',
253
+ 'materialize_cached_tree',
254
+ 'has_cached_install_tree',
255
+ 'install_from_cached_wheel',
256
+ ]
257
+