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,268 @@
1
+ """
2
+ #exonware/xwlazy/src/exonware/xwlazy/common/logger.py
3
+
4
+ Logging utilities for xwlazy - shared across package, module, and runtime.
5
+
6
+ Company: eXonware.com
7
+ Author: Eng. Muhammad AlShehri
8
+ Email: connect@exonware.com
9
+ Version: 0.1.0.19
10
+ Generation Date: 15-Nov-2025
11
+
12
+ This module provides unified logging functionality for all xwlazy components.
13
+ All logging code is centralized here to avoid duplication.
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import logging
19
+ import io
20
+ from typing import Dict, Optional
21
+ from datetime import datetime
22
+
23
+ # =============================================================================
24
+ # CONSTANTS
25
+ # =============================================================================
26
+
27
+ # Emoji mapping for log flags (shared across formatter and format_message)
28
+ _EMOJI_MAP = {
29
+ "WARN": "⚠️",
30
+ "INFO": "ℹ️",
31
+ "ACTION": "⚙️",
32
+ "SUCCESS": "✅",
33
+ "ERROR": "❌",
34
+ "FAIL": "⛔",
35
+ "DEBUG": "🔍",
36
+ "CRITICAL": "🚨",
37
+ }
38
+
39
+ # Default log category states
40
+ _CATEGORY_DEFAULTS: Dict[str, bool] = {
41
+ "install": True,
42
+ "hook": False,
43
+ "enhance": False,
44
+ "audit": False,
45
+ "sbom": False,
46
+ "config": False,
47
+ "discovery": False,
48
+ }
49
+
50
+ # =============================================================================
51
+ # MODULE STATE
52
+ # =============================================================================
53
+
54
+ _configured = False
55
+ _category_overrides: Dict[str, bool] = {}
56
+
57
+ # =============================================================================
58
+ # HELPER FUNCTIONS
59
+ # =============================================================================
60
+
61
+ def _normalize_category(name: str) -> str:
62
+ """Normalize category name to lowercase."""
63
+ return name.strip().lower()
64
+
65
+
66
+ def _load_env_overrides() -> None:
67
+ """Load log category overrides from environment variables."""
68
+ for category in _CATEGORY_DEFAULTS:
69
+ env_key = f"XWLAZY_LOG_{category.upper()}"
70
+ env_val = os.getenv(env_key)
71
+ if env_val is None:
72
+ continue
73
+ enabled = env_val.strip().lower() not in {"0", "false", "off", "no"}
74
+ _category_overrides[_normalize_category(category)] = enabled
75
+
76
+
77
+ # =============================================================================
78
+ # FORMATTER
79
+ # =============================================================================
80
+
81
+ class XWLazyFormatter(logging.Formatter):
82
+ """Custom formatter for xwlazy that uses exonware.xwlazy [HH:MM:SS]: [FLAG] format."""
83
+
84
+ LEVEL_FLAGS = {
85
+ logging.DEBUG: "DEBUG",
86
+ logging.INFO: "INFO",
87
+ logging.WARNING: "WARN",
88
+ logging.ERROR: "ERROR",
89
+ logging.CRITICAL: "CRITICAL",
90
+ }
91
+
92
+ def format(self, record: logging.LogRecord) -> str:
93
+ """Format log record with emoji and timestamp."""
94
+ flag = self.LEVEL_FLAGS.get(record.levelno, "INFO")
95
+ emoji = _EMOJI_MAP.get(flag, "ℹ️")
96
+ time_str = datetime.now().strftime("%H:%M:%S")
97
+ message = record.getMessage()
98
+ return f"{emoji} exonware.xwlazy [{time_str}]: [{flag}] {message}"
99
+
100
+
101
+ # =============================================================================
102
+ # CONFIGURATION
103
+ # =============================================================================
104
+
105
+ def _ensure_basic_config() -> None:
106
+ """Ensure logging is configured (called once)."""
107
+ global _configured
108
+ if _configured:
109
+ return
110
+
111
+ root_logger = logging.getLogger()
112
+ root_logger.setLevel(logging.INFO)
113
+
114
+ # Remove existing handlers to avoid duplicates
115
+ for handler in root_logger.handlers[:]:
116
+ root_logger.removeHandler(handler)
117
+
118
+ # Add console handler with custom formatter and UTF-8 encoding for Windows
119
+ # Wrap stdout with UTF-8 encoding to handle emoji characters on Windows
120
+ if sys.platform == "win32":
121
+ # On Windows, wrap stdout with UTF-8 encoding
122
+ try:
123
+ # Try to set UTF-8 encoding for stdout
124
+ if hasattr(sys.stdout, 'reconfigure'):
125
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
126
+ # Create a wrapper stream that handles encoding
127
+ utf8_stream = io.TextIOWrapper(
128
+ sys.stdout.buffer,
129
+ encoding='utf-8',
130
+ errors='replace',
131
+ line_buffering=True
132
+ )
133
+ console_handler = logging.StreamHandler(utf8_stream)
134
+ except (AttributeError, OSError):
135
+ # Fallback to regular stdout if reconfiguration fails
136
+ console_handler = logging.StreamHandler(sys.stdout)
137
+ else:
138
+ console_handler = logging.StreamHandler(sys.stdout)
139
+
140
+ console_handler.setLevel(logging.INFO)
141
+ console_handler.setFormatter(XWLazyFormatter())
142
+ root_logger.addHandler(console_handler)
143
+
144
+ # Load environment overrides
145
+ _load_env_overrides()
146
+
147
+ _configured = True
148
+
149
+
150
+ # =============================================================================
151
+ # PUBLIC API
152
+ # =============================================================================
153
+
154
+ def get_logger(name: Optional[str] = None) -> logging.Logger:
155
+ """
156
+ Return a logger configured for the lazy subsystem.
157
+
158
+ Args:
159
+ name: Optional logger name (defaults to "xwlazy.lazy")
160
+
161
+ Returns:
162
+ Configured logger instance
163
+ """
164
+ _ensure_basic_config()
165
+ return logging.getLogger(name or "xwlazy.lazy")
166
+
167
+
168
+ def is_log_category_enabled(category: str) -> bool:
169
+ """
170
+ Return True if the provided log category is enabled.
171
+
172
+ Args:
173
+ category: Log category name (e.g., "install", "hook")
174
+
175
+ Returns:
176
+ True if category is enabled, False otherwise
177
+ """
178
+ _ensure_basic_config()
179
+ normalized = _normalize_category(category)
180
+ if normalized in _category_overrides:
181
+ return _category_overrides[normalized]
182
+ return _CATEGORY_DEFAULTS.get(normalized, True)
183
+
184
+
185
+ def set_log_category(category: str, enabled: bool) -> None:
186
+ """
187
+ Enable/disable an individual log category at runtime.
188
+
189
+ Args:
190
+ category: Log category name
191
+ enabled: True to enable, False to disable
192
+ """
193
+ _category_overrides[_normalize_category(category)] = bool(enabled)
194
+
195
+
196
+ def set_log_categories(overrides: Dict[str, bool]) -> None:
197
+ """
198
+ Bulk update multiple log categories.
199
+
200
+ Args:
201
+ overrides: Dictionary mapping category names to enabled state
202
+ """
203
+ for category, enabled in overrides.items():
204
+ set_log_category(category, enabled)
205
+
206
+
207
+ def get_log_categories() -> Dict[str, bool]:
208
+ """
209
+ Return the effective state for each built-in log category.
210
+
211
+ Returns:
212
+ Dictionary mapping category names to enabled state
213
+ """
214
+ _ensure_basic_config()
215
+ result = {}
216
+ for category, default_enabled in _CATEGORY_DEFAULTS.items():
217
+ normalized = _normalize_category(category)
218
+ result[category] = _category_overrides.get(normalized, default_enabled)
219
+ return result
220
+
221
+
222
+ def log_event(category: str, level_fn, msg: str, *args, **kwargs) -> None:
223
+ """
224
+ Emit a log for the given category if it is enabled.
225
+
226
+ Args:
227
+ category: Log category name
228
+ level_fn: Logging function (e.g., logger.info, logger.warning)
229
+ msg: Log message format string
230
+ *args: Positional arguments for message formatting
231
+ **kwargs: Keyword arguments for message formatting
232
+ """
233
+ if is_log_category_enabled(category):
234
+ level_fn(msg, *args, **kwargs)
235
+
236
+
237
+ def format_message(flag: str, message: str) -> str:
238
+ """
239
+ Format a message with exonware.xwlazy [HH:MM:SS]: [FLAG] format.
240
+
241
+ Args:
242
+ flag: Log flag (e.g., "INFO", "WARN", "ERROR")
243
+ message: Message text
244
+
245
+ Returns:
246
+ Formatted message string
247
+ """
248
+ emoji = _EMOJI_MAP.get(flag, "ℹ️")
249
+ time_str = datetime.now().strftime("%H:%M:%S")
250
+ return f"{emoji} exonware.xwlazy [{time_str}]: [{flag}] {message}"
251
+
252
+
253
+ def print_formatted(flag: str, message: str, same_line: bool = False) -> None:
254
+ """
255
+ Print a formatted message with optional same-line support.
256
+
257
+ Args:
258
+ flag: Log flag (e.g., "INFO", "WARN", "ERROR")
259
+ message: Message text
260
+ same_line: If True, use carriage return for same-line output
261
+ """
262
+ formatted = format_message(flag, message)
263
+ if same_line:
264
+ sys.stdout.write(f"\r{formatted}")
265
+ sys.stdout.flush()
266
+ else:
267
+ print(formatted)
268
+
@@ -0,0 +1,72 @@
1
+ """
2
+ Common Services
3
+
4
+ Shared services used by both modules and packages.
5
+ """
6
+
7
+ from .dependency_mapper import DependencyMapper
8
+ from .spec_cache import (
9
+ _spec_cache_get,
10
+ _spec_cache_put,
11
+ _spec_cache_clear,
12
+ _cache_spec_if_missing,
13
+ get_stdlib_module_set,
14
+ )
15
+ from .state_manager import LazyStateManager
16
+ from .keyword_detection import (
17
+ enable_keyword_detection,
18
+ is_keyword_detection_enabled,
19
+ get_keyword_detection_keyword,
20
+ check_package_keywords,
21
+ _detect_lazy_installation,
22
+ _detect_meta_info_mode,
23
+ )
24
+ from .install_cache_utils import (
25
+ get_default_cache_dir,
26
+ get_cache_dir,
27
+ get_wheel_path,
28
+ get_install_tree_dir,
29
+ get_site_packages_dir,
30
+ pip_install_from_path,
31
+ ensure_cached_wheel,
32
+ install_from_cached_tree,
33
+ materialize_cached_tree,
34
+ has_cached_install_tree,
35
+ install_from_cached_wheel,
36
+ )
37
+ from .install_async_utils import (
38
+ get_package_size_mb,
39
+ async_install_package,
40
+ async_uninstall_package,
41
+ )
42
+
43
+ __all__ = [
44
+ 'DependencyMapper',
45
+ '_spec_cache_get',
46
+ '_spec_cache_put',
47
+ '_spec_cache_clear',
48
+ '_cache_spec_if_missing',
49
+ 'get_stdlib_module_set',
50
+ 'LazyStateManager',
51
+ 'enable_keyword_detection',
52
+ 'is_keyword_detection_enabled',
53
+ 'get_keyword_detection_keyword',
54
+ 'check_package_keywords',
55
+ '_detect_lazy_installation',
56
+ '_detect_meta_info_mode',
57
+ 'get_default_cache_dir',
58
+ 'get_cache_dir',
59
+ 'get_wheel_path',
60
+ 'get_install_tree_dir',
61
+ 'get_site_packages_dir',
62
+ 'pip_install_from_path',
63
+ 'ensure_cached_wheel',
64
+ 'install_from_cached_tree',
65
+ 'materialize_cached_tree',
66
+ 'has_cached_install_tree',
67
+ 'install_from_cached_wheel',
68
+ 'get_package_size_mb',
69
+ 'async_install_package',
70
+ 'async_uninstall_package',
71
+ ]
72
+
@@ -0,0 +1,234 @@
1
+ """
2
+ Dependency Mapper for package discovery.
3
+
4
+ This module contains DependencyMapper class extracted from lazy_core.py Section 1.
5
+ """
6
+
7
+ import threading
8
+ from typing import Dict, List, Optional, Set, Tuple
9
+
10
+ # Lazy import to avoid circular dependency
11
+ def _get_logger():
12
+ """Get logger (lazy import to avoid circular dependency)."""
13
+ from ...common.logger import get_logger
14
+ return get_logger("xwlazy.discovery")
15
+
16
+ logger = None # Will be initialized on first use
17
+
18
+ # Import from spec_cache (same directory)
19
+ from .spec_cache import (
20
+ _cached_stdlib_check,
21
+ _spec_cache_get,
22
+ _cache_spec_if_missing,
23
+ get_stdlib_module_set,
24
+ )
25
+
26
+ _STDLIB_MODULE_SET = get_stdlib_module_set()
27
+
28
+ # Import from manifest (package/services - avoid circular import)
29
+ def get_manifest_loader():
30
+ """Get manifest loader (lazy import to avoid circular dependency)."""
31
+ from ...package.services.manifest import get_manifest_loader as _get_manifest_loader
32
+ return _get_manifest_loader()
33
+
34
+ # Import from discovery (package/services - avoid circular import)
35
+ def get_lazy_discovery():
36
+ """Get discovery instance."""
37
+ from ...package.services.discovery import get_lazy_discovery as _get_lazy_discovery
38
+ return _get_lazy_discovery()
39
+
40
+
41
+ class DependencyMapper:
42
+ """
43
+ Maps import names to package names using dynamic discovery.
44
+ Optimized with caching to avoid repeated file I/O.
45
+ """
46
+
47
+ __slots__ = (
48
+ '_discovery',
49
+ '_package_import_mapping',
50
+ '_import_package_mapping',
51
+ '_cached',
52
+ '_lock',
53
+ '_package_name',
54
+ '_manifest_generation',
55
+ '_manifest_dependencies',
56
+ '_manifest_signature',
57
+ '_manifest_empty',
58
+ )
59
+
60
+ def __init__(self, package_name: str = 'default'):
61
+ """Initialize dependency mapper."""
62
+ self._discovery = None # Lazy init to avoid circular imports
63
+ self._package_import_mapping: Dict[str, List[str]] = {}
64
+ self._import_package_mapping: Dict[str, str] = {}
65
+ self._cached = False
66
+ self._lock = threading.RLock()
67
+ self._package_name = package_name
68
+ self._manifest_generation = -1
69
+ self._manifest_dependencies: Dict[str, str] = {}
70
+ self._manifest_signature: Optional[Tuple[str, float, float]] = None
71
+ self._manifest_empty = False
72
+
73
+ def set_package_name(self, package_name: str) -> None:
74
+ """Update the owning package name (affects manifest lookups)."""
75
+ normalized = (package_name or 'default').strip().lower() or 'default'
76
+ if normalized != self._package_name:
77
+ self._package_name = normalized
78
+ self._manifest_generation = -1
79
+ self._manifest_dependencies = {}
80
+
81
+ def _get_discovery(self):
82
+ """Get discovery instance (lazy init)."""
83
+ if self._discovery is None:
84
+ self._discovery = get_lazy_discovery()
85
+ return self._discovery
86
+
87
+ def _ensure_mappings_cached(self) -> None:
88
+ """Ensure mappings are cached (lazy initialization)."""
89
+ if self._cached:
90
+ return
91
+
92
+ with self._lock:
93
+ if self._cached:
94
+ return
95
+
96
+ discovery = self._get_discovery()
97
+ self._package_import_mapping = discovery.get_package_import_mapping()
98
+ self._import_package_mapping = discovery.get_import_package_mapping()
99
+ self._cached = True
100
+
101
+ def _ensure_manifest_cached(self, loader=None) -> None:
102
+ if loader is None:
103
+ loader = get_manifest_loader()
104
+ signature = loader.get_manifest_signature(self._package_name)
105
+ if signature == self._manifest_signature and (self._manifest_dependencies or self._manifest_empty):
106
+ return
107
+
108
+ shared = loader.get_shared_dependencies(self._package_name, signature)
109
+ if shared is not None:
110
+ self._manifest_generation = loader.generation
111
+ self._manifest_signature = signature
112
+ self._manifest_dependencies = shared
113
+ self._manifest_empty = len(shared) == 0
114
+ return
115
+
116
+ manifest = loader.get_manifest(self._package_name)
117
+ current_generation = loader.generation
118
+
119
+ dependencies: Dict[str, str] = {}
120
+ manifest_empty = True
121
+ if manifest and manifest.dependencies:
122
+ dependencies = {
123
+ key.lower(): value
124
+ for key, value in manifest.dependencies.items()
125
+ if key and value
126
+ }
127
+ manifest_empty = False
128
+
129
+ self._manifest_generation = current_generation
130
+ self._manifest_signature = signature
131
+ self._manifest_dependencies = dependencies
132
+ self._manifest_empty = manifest_empty
133
+
134
+ @staticmethod
135
+ def _is_stdlib_or_builtin(module_name: str) -> bool:
136
+ """Return True if the module is built-in or part of the stdlib."""
137
+ root = module_name.split('.', 1)[0]
138
+ needs_cache = False
139
+ if module_name in _STDLIB_MODULE_SET or root in _STDLIB_MODULE_SET:
140
+ return True
141
+ if _cached_stdlib_check(module_name):
142
+ needs_cache = True
143
+ if needs_cache:
144
+ _cache_spec_if_missing(module_name)
145
+ return needs_cache
146
+
147
+ DENY_LIST: Set[str] = {
148
+ # POSIX-only modules that don't exist on Windows but try to auto-install
149
+ "pwd",
150
+ "grp",
151
+ "spwd",
152
+ "nis",
153
+ "termios",
154
+ "tty",
155
+ "pty",
156
+ "fcntl",
157
+ # Windows-only internals
158
+ "winreg",
159
+ "winsound",
160
+ "_winapi",
161
+ "_dbm",
162
+ # Internal optional modules that must never trigger auto-install
163
+ "compression",
164
+ "socks",
165
+ "wimlib",
166
+ # Optional dependencies with Python 2 compatibility shims (Python 3.8+ only)
167
+ "inspect2", # Python 2 compatibility shim, not needed on Python 3.8+
168
+ "rich", # Optional CLI enhancement for httpx, not required for core functionality
169
+ }
170
+
171
+ def _should_skip_auto_install(self, import_name: str) -> bool:
172
+ """Determine whether an import should bypass lazy installation."""
173
+ global logger
174
+ if logger is None:
175
+ logger = _get_logger()
176
+
177
+ if self._is_stdlib_or_builtin(import_name):
178
+ logger.debug("Skipping lazy install for stdlib module '%s'", import_name)
179
+ return True
180
+
181
+ if import_name in self.DENY_LIST:
182
+ logger.debug("Skipping lazy install for denied module '%s'", import_name)
183
+ return True
184
+
185
+ return False
186
+
187
+ def get_package_name(self, import_name: str) -> Optional[str]:
188
+ """
189
+ Get package name from import name.
190
+
191
+ Priority order (manifest takes precedence):
192
+ 1. Skip checks (stdlib, deny list)
193
+ 2. Manifest dependencies (explicit user configuration - highest priority)
194
+ 3. Spec cache (module already exists - skip auto-install)
195
+ 4. Discovery mappings (automatic discovery)
196
+ """
197
+ if self._should_skip_auto_install(import_name):
198
+ return None
199
+
200
+ # Check manifest FIRST - explicit user configuration takes precedence
201
+ loader = get_manifest_loader()
202
+ generation_changed = self._manifest_generation != loader.generation
203
+ manifest_uninitialized = not self._manifest_dependencies and not self._manifest_empty
204
+ if generation_changed or manifest_uninitialized:
205
+ self._ensure_manifest_cached(loader)
206
+ manifest_hit = self._manifest_dependencies.get(import_name.lower())
207
+ if manifest_hit:
208
+ return manifest_hit
209
+
210
+ # Check spec cache - if module already exists, skip auto-install
211
+ if _spec_cache_get(import_name):
212
+ return None
213
+
214
+ self._ensure_mappings_cached()
215
+ return self._import_package_mapping.get(import_name, import_name)
216
+
217
+ def get_import_names(self, package_name: str) -> List[str]:
218
+ """Get all possible import names for a package."""
219
+ self._ensure_mappings_cached()
220
+ return self._package_import_mapping.get(package_name, [package_name])
221
+
222
+ def get_package_import_mapping(self) -> Dict[str, List[str]]:
223
+ """Get complete package to import names mapping."""
224
+ self._ensure_mappings_cached()
225
+ return self._package_import_mapping.copy()
226
+
227
+ def get_import_package_mapping(self) -> Dict[str, str]:
228
+ """Get complete import to package name mapping."""
229
+ self._ensure_mappings_cached()
230
+ return self._import_package_mapping.copy()
231
+
232
+
233
+ __all__ = ['DependencyMapper']
234
+