exonware-xwlazy 0.1.0.11__py3-none-any.whl → 0.1.0.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. exonware/__init__.py +26 -0
  2. exonware/xwlazy/__init__.py +0 -0
  3. exonware/xwlazy/common/__init__.py +47 -0
  4. exonware/xwlazy/common/base.py +56 -0
  5. exonware/xwlazy/common/cache.py +504 -0
  6. exonware/xwlazy/common/logger.py +257 -0
  7. exonware/xwlazy/common/services/__init__.py +72 -0
  8. exonware/xwlazy/common/services/dependency_mapper.py +232 -0
  9. exonware/xwlazy/common/services/install_async_utils.py +165 -0
  10. exonware/xwlazy/common/services/install_cache_utils.py +245 -0
  11. exonware/xwlazy/common/services/keyword_detection.py +283 -0
  12. exonware/xwlazy/common/services/spec_cache.py +165 -0
  13. xwlazy/lazy/lazy_state.py → exonware/xwlazy/common/services/state_manager.py +0 -2
  14. exonware/xwlazy/common/strategies/__init__.py +28 -0
  15. exonware/xwlazy/common/strategies/caching_dict.py +44 -0
  16. exonware/xwlazy/common/strategies/caching_installation.py +88 -0
  17. exonware/xwlazy/common/strategies/caching_lfu.py +66 -0
  18. exonware/xwlazy/common/strategies/caching_lru.py +63 -0
  19. exonware/xwlazy/common/strategies/caching_multitier.py +59 -0
  20. exonware/xwlazy/common/strategies/caching_ttl.py +59 -0
  21. {xwlazy/lazy → exonware/xwlazy}/config.py +51 -21
  22. exonware/xwlazy/contracts.py +1396 -0
  23. exonware/xwlazy/defs.py +378 -0
  24. xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -16
  25. exonware/xwlazy/facade.py +991 -0
  26. exonware/xwlazy/module/__init__.py +18 -0
  27. exonware/xwlazy/module/base.py +565 -0
  28. exonware/xwlazy/module/data.py +17 -0
  29. exonware/xwlazy/module/facade.py +246 -0
  30. exonware/xwlazy/module/importer_engine.py +2117 -0
  31. exonware/xwlazy/module/strategies/__init__.py +22 -0
  32. exonware/xwlazy/module/strategies/module_helper_lazy.py +93 -0
  33. exonware/xwlazy/module/strategies/module_helper_simple.py +65 -0
  34. exonware/xwlazy/module/strategies/module_manager_advanced.py +111 -0
  35. exonware/xwlazy/module/strategies/module_manager_simple.py +95 -0
  36. exonware/xwlazy/package/__init__.py +18 -0
  37. exonware/xwlazy/package/base.py +798 -0
  38. xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +61 -16
  39. exonware/xwlazy/package/data.py +17 -0
  40. exonware/xwlazy/package/facade.py +480 -0
  41. exonware/xwlazy/package/services/__init__.py +84 -0
  42. exonware/xwlazy/package/services/async_install_handle.py +87 -0
  43. exonware/xwlazy/package/services/config_manager.py +245 -0
  44. exonware/xwlazy/package/services/discovery.py +370 -0
  45. {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -20
  46. exonware/xwlazy/package/services/install_async.py +277 -0
  47. exonware/xwlazy/package/services/install_cache.py +145 -0
  48. exonware/xwlazy/package/services/install_interactive.py +59 -0
  49. exonware/xwlazy/package/services/install_policy.py +156 -0
  50. exonware/xwlazy/package/services/install_registry.py +54 -0
  51. exonware/xwlazy/package/services/install_result.py +17 -0
  52. exonware/xwlazy/package/services/install_sbom.py +153 -0
  53. exonware/xwlazy/package/services/install_utils.py +79 -0
  54. exonware/xwlazy/package/services/installer_engine.py +406 -0
  55. exonware/xwlazy/package/services/lazy_installer.py +718 -0
  56. {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +40 -33
  57. exonware/xwlazy/package/services/strategy_registry.py +186 -0
  58. exonware/xwlazy/package/strategies/__init__.py +57 -0
  59. exonware/xwlazy/package/strategies/package_discovery_file.py +129 -0
  60. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +84 -0
  61. exonware/xwlazy/package/strategies/package_discovery_manifest.py +101 -0
  62. exonware/xwlazy/package/strategies/package_execution_async.py +113 -0
  63. exonware/xwlazy/package/strategies/package_execution_cached.py +90 -0
  64. exonware/xwlazy/package/strategies/package_execution_pip.py +99 -0
  65. exonware/xwlazy/package/strategies/package_execution_wheel.py +106 -0
  66. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +100 -0
  67. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +105 -0
  68. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +100 -0
  69. exonware/xwlazy/package/strategies/package_policy_allow_list.py +57 -0
  70. exonware/xwlazy/package/strategies/package_policy_deny_list.py +57 -0
  71. exonware/xwlazy/package/strategies/package_policy_permissive.py +46 -0
  72. exonware/xwlazy/package/strategies/package_timing_clean.py +67 -0
  73. exonware/xwlazy/package/strategies/package_timing_full.py +66 -0
  74. exonware/xwlazy/package/strategies/package_timing_smart.py +68 -0
  75. exonware/xwlazy/package/strategies/package_timing_temporary.py +66 -0
  76. exonware/xwlazy/runtime/__init__.py +18 -0
  77. exonware/xwlazy/runtime/adaptive_learner.py +129 -0
  78. exonware/xwlazy/runtime/base.py +274 -0
  79. exonware/xwlazy/runtime/facade.py +94 -0
  80. exonware/xwlazy/runtime/intelligent_selector.py +170 -0
  81. exonware/xwlazy/runtime/metrics.py +60 -0
  82. exonware/xwlazy/runtime/performance.py +37 -0
  83. exonware/xwlazy/version.py +2 -2
  84. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/METADATA +89 -11
  85. exonware_xwlazy-0.1.0.20.dist-info/RECORD +87 -0
  86. exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
  87. xwlazy/__init__.py +0 -34
  88. xwlazy/lazy/__init__.py +0 -301
  89. xwlazy/lazy/bootstrap.py +0 -106
  90. xwlazy/lazy/lazy_base.py +0 -465
  91. xwlazy/lazy/lazy_contracts.py +0 -290
  92. xwlazy/lazy/lazy_core.py +0 -3727
  93. xwlazy/lazy/logging_utils.py +0 -194
  94. xwlazy/version.py +0 -77
  95. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/WHEEL +0 -0
  96. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.20.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,257 @@
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
+
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
+ def _load_env_overrides() -> None:
66
+ """Load log category overrides from environment variables."""
67
+ for category in _CATEGORY_DEFAULTS:
68
+ env_key = f"XWLAZY_LOG_{category.upper()}"
69
+ env_val = os.getenv(env_key)
70
+ if env_val is None:
71
+ continue
72
+ enabled = env_val.strip().lower() not in {"0", "false", "off", "no"}
73
+ _category_overrides[_normalize_category(category)] = enabled
74
+
75
+ # =============================================================================
76
+ # FORMATTER
77
+ # =============================================================================
78
+
79
+ class XWLazyFormatter(logging.Formatter):
80
+ """Custom formatter for xwlazy that uses exonware.xwlazy [HH:MM:SS]: [FLAG] format."""
81
+
82
+ LEVEL_FLAGS = {
83
+ logging.DEBUG: "DEBUG",
84
+ logging.INFO: "INFO",
85
+ logging.WARNING: "WARN",
86
+ logging.ERROR: "ERROR",
87
+ logging.CRITICAL: "CRITICAL",
88
+ }
89
+
90
+ def format(self, record: logging.LogRecord) -> str:
91
+ """Format log record with emoji and timestamp."""
92
+ flag = self.LEVEL_FLAGS.get(record.levelno, "INFO")
93
+ emoji = _EMOJI_MAP.get(flag, "ℹ️")
94
+ time_str = datetime.now().strftime("%H:%M:%S")
95
+ message = record.getMessage()
96
+ return f"{emoji} exonware.xwlazy [{time_str}]: [{flag}] {message}"
97
+
98
+ # =============================================================================
99
+ # CONFIGURATION
100
+ # =============================================================================
101
+
102
+ def _ensure_basic_config() -> None:
103
+ """Ensure logging is configured (called once)."""
104
+ global _configured
105
+ if _configured:
106
+ return
107
+
108
+ root_logger = logging.getLogger()
109
+ root_logger.setLevel(logging.INFO)
110
+
111
+ # Remove existing handlers to avoid duplicates
112
+ for handler in root_logger.handlers[:]:
113
+ root_logger.removeHandler(handler)
114
+
115
+ # Add console handler with custom formatter and UTF-8 encoding for Windows
116
+ # Wrap stdout with UTF-8 encoding to handle emoji characters on Windows
117
+ if sys.platform == "win32":
118
+ # On Windows, wrap stdout with UTF-8 encoding
119
+ try:
120
+ # Try to set UTF-8 encoding for stdout
121
+ if hasattr(sys.stdout, 'reconfigure'):
122
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
123
+ # Create a wrapper stream that handles encoding
124
+ utf8_stream = io.TextIOWrapper(
125
+ sys.stdout.buffer,
126
+ encoding='utf-8',
127
+ errors='replace',
128
+ line_buffering=True
129
+ )
130
+ console_handler = logging.StreamHandler(utf8_stream)
131
+ except (AttributeError, OSError):
132
+ # Fallback to regular stdout if reconfiguration fails
133
+ console_handler = logging.StreamHandler(sys.stdout)
134
+ else:
135
+ console_handler = logging.StreamHandler(sys.stdout)
136
+
137
+ console_handler.setLevel(logging.INFO)
138
+ console_handler.setFormatter(XWLazyFormatter())
139
+ root_logger.addHandler(console_handler)
140
+
141
+ # Load environment overrides
142
+ _load_env_overrides()
143
+
144
+ _configured = True
145
+
146
+ # =============================================================================
147
+ # PUBLIC API
148
+ # =============================================================================
149
+
150
+ def get_logger(name: Optional[str] = None) -> logging.Logger:
151
+ """
152
+ Return a logger configured for the lazy subsystem.
153
+
154
+ Args:
155
+ name: Optional logger name (defaults to "xwlazy.lazy")
156
+
157
+ Returns:
158
+ Configured logger instance
159
+ """
160
+ _ensure_basic_config()
161
+ return logging.getLogger(name or "xwlazy.lazy")
162
+
163
+ def is_log_category_enabled(category: str) -> bool:
164
+ """
165
+ Return True if the provided log category is enabled.
166
+
167
+ Args:
168
+ category: Log category name (e.g., "install", "hook")
169
+
170
+ Returns:
171
+ True if category is enabled, False otherwise
172
+ """
173
+ _ensure_basic_config()
174
+ normalized = _normalize_category(category)
175
+ if normalized in _category_overrides:
176
+ return _category_overrides[normalized]
177
+ return _CATEGORY_DEFAULTS.get(normalized, True)
178
+
179
+ def set_log_category(category: str, enabled: bool) -> None:
180
+ """
181
+ Enable/disable an individual log category at runtime.
182
+
183
+ Args:
184
+ category: Log category name
185
+ enabled: True to enable, False to disable
186
+ """
187
+ _category_overrides[_normalize_category(category)] = bool(enabled)
188
+
189
+ def set_log_categories(overrides: Dict[str, bool]) -> None:
190
+ """
191
+ Bulk update multiple log categories.
192
+
193
+ Args:
194
+ overrides: Dictionary mapping category names to enabled state
195
+ """
196
+ for category, enabled in overrides.items():
197
+ set_log_category(category, enabled)
198
+
199
+ def get_log_categories() -> Dict[str, bool]:
200
+ """
201
+ Return the effective state for each built-in log category.
202
+
203
+ Returns:
204
+ Dictionary mapping category names to enabled state
205
+ """
206
+ _ensure_basic_config()
207
+ result = {}
208
+ for category, default_enabled in _CATEGORY_DEFAULTS.items():
209
+ normalized = _normalize_category(category)
210
+ result[category] = _category_overrides.get(normalized, default_enabled)
211
+ return result
212
+
213
+ def log_event(category: str, level_fn, msg: str, *args, **kwargs) -> None:
214
+ """
215
+ Emit a log for the given category if it is enabled.
216
+
217
+ Args:
218
+ category: Log category name
219
+ level_fn: Logging function (e.g., logger.info, logger.warning)
220
+ msg: Log message format string
221
+ *args: Positional arguments for message formatting
222
+ **kwargs: Keyword arguments for message formatting
223
+ """
224
+ if is_log_category_enabled(category):
225
+ level_fn(msg, *args, **kwargs)
226
+
227
+ def format_message(flag: str, message: str) -> str:
228
+ """
229
+ Format a message with exonware.xwlazy [HH:MM:SS]: [FLAG] format.
230
+
231
+ Args:
232
+ flag: Log flag (e.g., "INFO", "WARN", "ERROR")
233
+ message: Message text
234
+
235
+ Returns:
236
+ Formatted message string
237
+ """
238
+ emoji = _EMOJI_MAP.get(flag, "ℹ️")
239
+ time_str = datetime.now().strftime("%H:%M:%S")
240
+ return f"{emoji} exonware.xwlazy [{time_str}]: [{flag}] {message}"
241
+
242
+ def print_formatted(flag: str, message: str, same_line: bool = False) -> None:
243
+ """
244
+ Print a formatted message with optional same-line support.
245
+
246
+ Args:
247
+ flag: Log flag (e.g., "INFO", "WARN", "ERROR")
248
+ message: Message text
249
+ same_line: If True, use carriage return for same-line output
250
+ """
251
+ formatted = format_message(flag, message)
252
+ if same_line:
253
+ sys.stdout.write(f"\r{formatted}")
254
+ sys.stdout.flush()
255
+ else:
256
+ print(formatted)
257
+
@@ -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,232 @@
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
+ class DependencyMapper:
41
+ """
42
+ Maps import names to package names using dynamic discovery.
43
+ Optimized with caching to avoid repeated file I/O.
44
+ """
45
+
46
+ __slots__ = (
47
+ '_discovery',
48
+ '_package_import_mapping',
49
+ '_import_package_mapping',
50
+ '_cached',
51
+ '_lock',
52
+ '_package_name',
53
+ '_manifest_generation',
54
+ '_manifest_dependencies',
55
+ '_manifest_signature',
56
+ '_manifest_empty',
57
+ )
58
+
59
+ def __init__(self, package_name: str = 'default'):
60
+ """Initialize dependency mapper."""
61
+ self._discovery = None # Lazy init to avoid circular imports
62
+ self._package_import_mapping: Dict[str, List[str]] = {}
63
+ self._import_package_mapping: Dict[str, str] = {}
64
+ self._cached = False
65
+ self._lock = threading.RLock()
66
+ self._package_name = package_name
67
+ self._manifest_generation = -1
68
+ self._manifest_dependencies: Dict[str, str] = {}
69
+ self._manifest_signature: Optional[Tuple[str, float, float]] = None
70
+ self._manifest_empty = False
71
+
72
+ def set_package_name(self, package_name: str) -> None:
73
+ """Update the owning package name (affects manifest lookups)."""
74
+ normalized = (package_name or 'default').strip().lower() or 'default'
75
+ if normalized != self._package_name:
76
+ self._package_name = normalized
77
+ self._manifest_generation = -1
78
+ self._manifest_dependencies = {}
79
+
80
+ def _get_discovery(self):
81
+ """Get discovery instance (lazy init)."""
82
+ if self._discovery is None:
83
+ self._discovery = get_lazy_discovery()
84
+ return self._discovery
85
+
86
+ def _ensure_mappings_cached(self) -> None:
87
+ """Ensure mappings are cached (lazy initialization)."""
88
+ if self._cached:
89
+ return
90
+
91
+ with self._lock:
92
+ if self._cached:
93
+ return
94
+
95
+ discovery = self._get_discovery()
96
+ self._package_import_mapping = discovery.get_package_import_mapping()
97
+ self._import_package_mapping = discovery.get_import_package_mapping()
98
+ self._cached = True
99
+
100
+ def _ensure_manifest_cached(self, loader=None) -> None:
101
+ if loader is None:
102
+ loader = get_manifest_loader()
103
+ signature = loader.get_manifest_signature(self._package_name)
104
+ if signature == self._manifest_signature and (self._manifest_dependencies or self._manifest_empty):
105
+ return
106
+
107
+ shared = loader.get_shared_dependencies(self._package_name, signature)
108
+ if shared is not None:
109
+ self._manifest_generation = loader.generation
110
+ self._manifest_signature = signature
111
+ self._manifest_dependencies = shared
112
+ self._manifest_empty = len(shared) == 0
113
+ return
114
+
115
+ manifest = loader.get_manifest(self._package_name)
116
+ current_generation = loader.generation
117
+
118
+ dependencies: Dict[str, str] = {}
119
+ manifest_empty = True
120
+ if manifest and manifest.dependencies:
121
+ dependencies = {
122
+ key.lower(): value
123
+ for key, value in manifest.dependencies.items()
124
+ if key and value
125
+ }
126
+ manifest_empty = False
127
+
128
+ self._manifest_generation = current_generation
129
+ self._manifest_signature = signature
130
+ self._manifest_dependencies = dependencies
131
+ self._manifest_empty = manifest_empty
132
+
133
+ @staticmethod
134
+ def _is_stdlib_or_builtin(module_name: str) -> bool:
135
+ """Return True if the module is built-in or part of the stdlib."""
136
+ root = module_name.split('.', 1)[0]
137
+ needs_cache = False
138
+ if module_name in _STDLIB_MODULE_SET or root in _STDLIB_MODULE_SET:
139
+ return True
140
+ if _cached_stdlib_check(module_name):
141
+ needs_cache = True
142
+ if needs_cache:
143
+ _cache_spec_if_missing(module_name)
144
+ return needs_cache
145
+
146
+ DENY_LIST: Set[str] = {
147
+ # POSIX-only modules that don't exist on Windows but try to auto-install
148
+ "pwd",
149
+ "grp",
150
+ "spwd",
151
+ "nis",
152
+ "termios",
153
+ "tty",
154
+ "pty",
155
+ "fcntl",
156
+ # Windows-only internals
157
+ "winreg",
158
+ "winsound",
159
+ "_winapi",
160
+ "_dbm",
161
+ # Internal optional modules that must never trigger auto-install
162
+ "compression",
163
+ "socks",
164
+ "wimlib",
165
+ # Optional dependencies with Python 2 compatibility shims (Python 3.8+ only)
166
+ "inspect2", # Python 2 compatibility shim, not needed on Python 3.8+
167
+ "rich", # Optional CLI enhancement for httpx, not required for core functionality
168
+ }
169
+
170
+ def _should_skip_auto_install(self, import_name: str) -> bool:
171
+ """Determine whether an import should bypass lazy installation."""
172
+ global logger
173
+ if logger is None:
174
+ logger = _get_logger()
175
+
176
+ if self._is_stdlib_or_builtin(import_name):
177
+ logger.debug("Skipping lazy install for stdlib module '%s'", import_name)
178
+ return True
179
+
180
+ if import_name in self.DENY_LIST:
181
+ logger.debug("Skipping lazy install for denied module '%s'", import_name)
182
+ return True
183
+
184
+ return False
185
+
186
+ def get_package_name(self, import_name: str) -> Optional[str]:
187
+ """
188
+ Get package name from import name.
189
+
190
+ Priority order (manifest takes precedence):
191
+ 1. Skip checks (stdlib, deny list)
192
+ 2. Manifest dependencies (explicit user configuration - highest priority)
193
+ 3. Spec cache (module already exists - skip auto-install)
194
+ 4. Discovery mappings (automatic discovery)
195
+ """
196
+ if self._should_skip_auto_install(import_name):
197
+ return None
198
+
199
+ # Check manifest FIRST - explicit user configuration takes precedence
200
+ loader = get_manifest_loader()
201
+ generation_changed = self._manifest_generation != loader.generation
202
+ manifest_uninitialized = not self._manifest_dependencies and not self._manifest_empty
203
+ if generation_changed or manifest_uninitialized:
204
+ self._ensure_manifest_cached(loader)
205
+ manifest_hit = self._manifest_dependencies.get(import_name.lower())
206
+ if manifest_hit:
207
+ return manifest_hit
208
+
209
+ # Check spec cache - if module already exists, skip auto-install
210
+ if _spec_cache_get(import_name):
211
+ return None
212
+
213
+ self._ensure_mappings_cached()
214
+ return self._import_package_mapping.get(import_name, import_name)
215
+
216
+ def get_import_names(self, package_name: str) -> List[str]:
217
+ """Get all possible import names for a package."""
218
+ self._ensure_mappings_cached()
219
+ return self._package_import_mapping.get(package_name, [package_name])
220
+
221
+ def get_package_import_mapping(self) -> Dict[str, List[str]]:
222
+ """Get complete package to import names mapping."""
223
+ self._ensure_mappings_cached()
224
+ return self._package_import_mapping.copy()
225
+
226
+ def get_import_package_mapping(self) -> Dict[str, str]:
227
+ """Get complete import to package name mapping."""
228
+ self._ensure_mappings_cached()
229
+ return self._import_package_mapping.copy()
230
+
231
+ __all__ = ['DependencyMapper']
232
+