exonware-xwlazy 0.1.0.22__py3-none-any.whl → 1.0.1.2__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 +86 -16
- exonware/xwlazy/version.py +5 -5
- exonware/xwlazy.py +2546 -0
- exonware/xwlazy_external_libs.toml +716 -0
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/METADATA +6 -6
- exonware_xwlazy-1.0.1.2.dist-info/RECORD +8 -0
- exonware/xwlazy/__init__.py +0 -367
- exonware/xwlazy/common/__init__.py +0 -47
- exonware/xwlazy/common/base.py +0 -56
- exonware/xwlazy/common/cache.py +0 -504
- exonware/xwlazy/common/logger.py +0 -257
- exonware/xwlazy/common/services/__init__.py +0 -72
- exonware/xwlazy/common/services/dependency_mapper.py +0 -232
- exonware/xwlazy/common/services/install_async_utils.py +0 -165
- exonware/xwlazy/common/services/install_cache_utils.py +0 -245
- exonware/xwlazy/common/services/keyword_detection.py +0 -283
- exonware/xwlazy/common/services/spec_cache.py +0 -165
- exonware/xwlazy/common/services/state_manager.py +0 -84
- exonware/xwlazy/common/strategies/__init__.py +0 -28
- exonware/xwlazy/common/strategies/caching_dict.py +0 -44
- exonware/xwlazy/common/strategies/caching_installation.py +0 -88
- exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
- exonware/xwlazy/common/strategies/caching_lru.py +0 -63
- exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
- exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
- exonware/xwlazy/config.py +0 -193
- exonware/xwlazy/contracts.py +0 -1396
- exonware/xwlazy/defs.py +0 -378
- exonware/xwlazy/errors.py +0 -276
- exonware/xwlazy/facade.py +0 -991
- exonware/xwlazy/module/__init__.py +0 -18
- exonware/xwlazy/module/base.py +0 -565
- exonware/xwlazy/module/data.py +0 -17
- exonware/xwlazy/module/facade.py +0 -246
- exonware/xwlazy/module/importer_engine.py +0 -2117
- exonware/xwlazy/module/strategies/__init__.py +0 -22
- exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
- exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
- exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
- exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
- exonware/xwlazy/package/__init__.py +0 -18
- exonware/xwlazy/package/base.py +0 -798
- exonware/xwlazy/package/conf.py +0 -324
- exonware/xwlazy/package/data.py +0 -17
- exonware/xwlazy/package/facade.py +0 -480
- exonware/xwlazy/package/services/__init__.py +0 -84
- exonware/xwlazy/package/services/async_install_handle.py +0 -87
- exonware/xwlazy/package/services/config_manager.py +0 -245
- exonware/xwlazy/package/services/discovery.py +0 -370
- exonware/xwlazy/package/services/host_packages.py +0 -145
- exonware/xwlazy/package/services/install_async.py +0 -277
- exonware/xwlazy/package/services/install_cache.py +0 -145
- exonware/xwlazy/package/services/install_interactive.py +0 -59
- exonware/xwlazy/package/services/install_policy.py +0 -156
- exonware/xwlazy/package/services/install_registry.py +0 -54
- exonware/xwlazy/package/services/install_result.py +0 -17
- exonware/xwlazy/package/services/install_sbom.py +0 -153
- exonware/xwlazy/package/services/install_utils.py +0 -79
- exonware/xwlazy/package/services/installer_engine.py +0 -406
- exonware/xwlazy/package/services/lazy_installer.py +0 -718
- exonware/xwlazy/package/services/manifest.py +0 -496
- exonware/xwlazy/package/services/strategy_registry.py +0 -186
- exonware/xwlazy/package/strategies/__init__.py +0 -57
- exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
- exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
- exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
- exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
- exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
- exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
- exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
- exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
- exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
- exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
- exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
- exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
- exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
- exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
- exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
- exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
- exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
- exonware/xwlazy/runtime/__init__.py +0 -18
- exonware/xwlazy/runtime/adaptive_learner.py +0 -129
- exonware/xwlazy/runtime/base.py +0 -274
- exonware/xwlazy/runtime/facade.py +0 -94
- exonware/xwlazy/runtime/intelligent_selector.py +0 -170
- exonware/xwlazy/runtime/metrics.py +0 -60
- exonware/xwlazy/runtime/performance.py +0 -37
- exonware_xwlazy-0.1.0.22.dist-info/RECORD +0 -87
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/WHEEL +0 -0
- {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Configuration management for lazy loading system.
|
|
3
|
-
|
|
4
|
-
This module contains LazyInstallConfig which manages per-package lazy installation
|
|
5
|
-
configuration. Extracted from lazy_core.py Section 5.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import Dict, Optional
|
|
9
|
-
from ...common.services import LazyStateManager
|
|
10
|
-
from ...defs import LazyLoadMode, LazyInstallMode, LazyModeConfig
|
|
11
|
-
from ...defs import get_preset_mode
|
|
12
|
-
|
|
13
|
-
# Lazy import to avoid circular dependency
|
|
14
|
-
def _get_logger():
|
|
15
|
-
"""Get logger (lazy import to avoid circular dependency)."""
|
|
16
|
-
from ...common.logger import get_logger
|
|
17
|
-
return get_logger("xwlazy.config")
|
|
18
|
-
|
|
19
|
-
def _get_log_event():
|
|
20
|
-
"""Get log_event function (lazy import to avoid circular dependency)."""
|
|
21
|
-
from ...common.logger import log_event
|
|
22
|
-
return log_event
|
|
23
|
-
|
|
24
|
-
logger = None # Will be initialized on first use
|
|
25
|
-
_log = None # Will be initialized on first use
|
|
26
|
-
|
|
27
|
-
# Mode enum mapping - extracted from lazy_core.py
|
|
28
|
-
_MODE_ENUM_MAP = {
|
|
29
|
-
# Core v1.0 modes
|
|
30
|
-
"none": LazyInstallMode.NONE,
|
|
31
|
-
"smart": LazyInstallMode.SMART,
|
|
32
|
-
"full": LazyInstallMode.FULL,
|
|
33
|
-
"clean": LazyInstallMode.CLEAN,
|
|
34
|
-
"temporary": LazyInstallMode.TEMPORARY,
|
|
35
|
-
"size_aware": LazyInstallMode.SIZE_AWARE,
|
|
36
|
-
# Special purpose modes
|
|
37
|
-
"interactive": LazyInstallMode.INTERACTIVE,
|
|
38
|
-
"warn": LazyInstallMode.WARN,
|
|
39
|
-
"disabled": LazyInstallMode.DISABLED,
|
|
40
|
-
"dry_run": LazyInstallMode.DRY_RUN,
|
|
41
|
-
# Legacy aliases
|
|
42
|
-
"auto": LazyInstallMode.SMART,
|
|
43
|
-
"on_demand": LazyInstallMode.SMART,
|
|
44
|
-
"on-demand": LazyInstallMode.SMART,
|
|
45
|
-
"lazy": LazyInstallMode.SMART,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
class LazyInstallConfig:
|
|
49
|
-
"""Global configuration for lazy installation per package."""
|
|
50
|
-
_configs: Dict[str, bool] = {}
|
|
51
|
-
_modes: Dict[str, str] = {}
|
|
52
|
-
_load_modes: Dict[str, LazyLoadMode] = {}
|
|
53
|
-
_install_modes: Dict[str, LazyInstallMode] = {}
|
|
54
|
-
_mode_configs: Dict[str, LazyModeConfig] = {}
|
|
55
|
-
_initialized: Dict[str, bool] = {}
|
|
56
|
-
_manual_overrides: Dict[str, bool] = {}
|
|
57
|
-
|
|
58
|
-
@classmethod
|
|
59
|
-
def set(
|
|
60
|
-
cls,
|
|
61
|
-
package_name: str,
|
|
62
|
-
enabled: bool,
|
|
63
|
-
mode: str = "auto",
|
|
64
|
-
install_hook: bool = True,
|
|
65
|
-
manual: bool = False,
|
|
66
|
-
load_mode: Optional[LazyLoadMode] = None,
|
|
67
|
-
install_mode: Optional[LazyInstallMode] = None,
|
|
68
|
-
mode_config: Optional[LazyModeConfig] = None,
|
|
69
|
-
) -> None:
|
|
70
|
-
"""Enable or disable lazy installation for a specific package."""
|
|
71
|
-
package_key = package_name.lower()
|
|
72
|
-
state_manager = LazyStateManager(package_name)
|
|
73
|
-
|
|
74
|
-
if manual:
|
|
75
|
-
cls._manual_overrides[package_key] = True
|
|
76
|
-
state_manager.set_manual_state(enabled)
|
|
77
|
-
elif cls._manual_overrides.get(package_key):
|
|
78
|
-
global logger
|
|
79
|
-
if logger is None:
|
|
80
|
-
logger = _get_logger()
|
|
81
|
-
logger.debug(
|
|
82
|
-
f"Lazy install config for {package_key} already overridden manually; skipping auto configuration."
|
|
83
|
-
)
|
|
84
|
-
return
|
|
85
|
-
else:
|
|
86
|
-
state_manager.set_manual_state(None)
|
|
87
|
-
|
|
88
|
-
cls._configs[package_key] = enabled
|
|
89
|
-
cls._modes[package_key] = mode
|
|
90
|
-
|
|
91
|
-
# Handle two-dimensional mode configuration
|
|
92
|
-
if mode_config:
|
|
93
|
-
cls._mode_configs[package_key] = mode_config
|
|
94
|
-
cls._load_modes[package_key] = mode_config.load_mode
|
|
95
|
-
cls._install_modes[package_key] = mode_config.install_mode
|
|
96
|
-
elif load_mode is not None or install_mode is not None:
|
|
97
|
-
# Explicit mode specification
|
|
98
|
-
if load_mode is None:
|
|
99
|
-
load_mode = LazyLoadMode.AUTO # Default
|
|
100
|
-
if install_mode is None:
|
|
101
|
-
install_mode = _MODE_ENUM_MAP.get(mode.lower(), LazyInstallMode.SMART)
|
|
102
|
-
cls._load_modes[package_key] = load_mode
|
|
103
|
-
cls._install_modes[package_key] = install_mode
|
|
104
|
-
cls._mode_configs[package_key] = LazyModeConfig(
|
|
105
|
-
load_mode=load_mode,
|
|
106
|
-
install_mode=install_mode
|
|
107
|
-
)
|
|
108
|
-
else:
|
|
109
|
-
# Legacy mode string - try to resolve to preset or default
|
|
110
|
-
preset = get_preset_mode(mode)
|
|
111
|
-
if preset:
|
|
112
|
-
cls._mode_configs[package_key] = preset
|
|
113
|
-
cls._load_modes[package_key] = preset.load_mode
|
|
114
|
-
cls._install_modes[package_key] = preset.install_mode
|
|
115
|
-
else:
|
|
116
|
-
# Fallback to legacy behavior
|
|
117
|
-
install_mode_enum = _MODE_ENUM_MAP.get(mode.lower(), LazyInstallMode.SMART)
|
|
118
|
-
cls._load_modes[package_key] = LazyLoadMode.AUTO
|
|
119
|
-
cls._install_modes[package_key] = install_mode_enum
|
|
120
|
-
cls._mode_configs[package_key] = LazyModeConfig(
|
|
121
|
-
load_mode=LazyLoadMode.AUTO,
|
|
122
|
-
install_mode=install_mode_enum
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
cls._initialize_package(package_key, enabled, mode, install_hook=install_hook)
|
|
126
|
-
|
|
127
|
-
@classmethod
|
|
128
|
-
def _initialize_package(cls, package_key: str, enabled: bool, mode: str, install_hook: bool = True) -> None:
|
|
129
|
-
"""Initialize lazy installation for a specific package."""
|
|
130
|
-
global logger, _log
|
|
131
|
-
if logger is None:
|
|
132
|
-
logger = _get_logger()
|
|
133
|
-
if _log is None:
|
|
134
|
-
_log = _get_log_event()
|
|
135
|
-
|
|
136
|
-
# Deferred imports to avoid circular dependency
|
|
137
|
-
from .install_registry import LazyInstallerRegistry
|
|
138
|
-
from ...facade import (
|
|
139
|
-
enable_lazy_install,
|
|
140
|
-
disable_lazy_install,
|
|
141
|
-
set_lazy_install_mode,
|
|
142
|
-
enable_lazy_imports,
|
|
143
|
-
install_import_hook,
|
|
144
|
-
uninstall_import_hook,
|
|
145
|
-
is_import_hook_installed,
|
|
146
|
-
sync_manifest_configuration,
|
|
147
|
-
)
|
|
148
|
-
import asyncio
|
|
149
|
-
|
|
150
|
-
if enabled:
|
|
151
|
-
try:
|
|
152
|
-
# Don't call enable_lazy_install() here - it would create infinite recursion
|
|
153
|
-
# The config is already set by LazyInstallConfig.set() above
|
|
154
|
-
|
|
155
|
-
# Use explicitly set install_mode from config, or derive from mode string
|
|
156
|
-
# Check if install_mode was explicitly set by checking if package_key exists in _install_modes
|
|
157
|
-
if package_key in cls._install_modes:
|
|
158
|
-
# install_mode was explicitly set in set() method, don't override it
|
|
159
|
-
mode_enum = cls._install_modes[package_key]
|
|
160
|
-
else:
|
|
161
|
-
# Not explicitly set, derive from mode string
|
|
162
|
-
mode_enum = _MODE_ENUM_MAP.get(mode.lower(), LazyInstallMode.SMART)
|
|
163
|
-
set_lazy_install_mode(package_key, mode_enum)
|
|
164
|
-
|
|
165
|
-
# Get load mode from config
|
|
166
|
-
load_mode = cls.get_load_mode(package_key)
|
|
167
|
-
|
|
168
|
-
# Enable lazy imports with appropriate load mode (skip if NONE mode)
|
|
169
|
-
if load_mode != LazyLoadMode.NONE:
|
|
170
|
-
enable_lazy_imports(load_mode, package_name=package_key)
|
|
171
|
-
|
|
172
|
-
# Enable async for modes that support it
|
|
173
|
-
installer = LazyInstallerRegistry.get_instance(package_key)
|
|
174
|
-
if installer and mode_enum in (LazyInstallMode.SMART, LazyInstallMode.FULL, LazyInstallMode.CLEAN, LazyInstallMode.TEMPORARY):
|
|
175
|
-
installer._async_enabled = True
|
|
176
|
-
installer._ensure_async_loop()
|
|
177
|
-
|
|
178
|
-
# For FULL mode, install all dependencies on start
|
|
179
|
-
if mode_enum == LazyInstallMode.FULL:
|
|
180
|
-
loop = installer._async_loop
|
|
181
|
-
if loop:
|
|
182
|
-
asyncio.run_coroutine_threadsafe(installer.install_all_dependencies(), loop)
|
|
183
|
-
|
|
184
|
-
if install_hook:
|
|
185
|
-
if not is_import_hook_installed(package_key):
|
|
186
|
-
install_import_hook(package_key)
|
|
187
|
-
_log("config", logger.info, f"✅ Lazy installation initialized for {package_key} (install_mode: {mode}, load_mode: {load_mode.value}, hook: installed)")
|
|
188
|
-
else:
|
|
189
|
-
uninstall_import_hook(package_key)
|
|
190
|
-
_log("config", logger.info, f"✅ Lazy installation initialized for {package_key} (install_mode: {mode}, load_mode: {load_mode.value}, hook: disabled)")
|
|
191
|
-
|
|
192
|
-
cls._initialized[package_key] = True
|
|
193
|
-
sync_manifest_configuration(package_key)
|
|
194
|
-
except ImportError as e:
|
|
195
|
-
if logger is None:
|
|
196
|
-
logger = _get_logger()
|
|
197
|
-
logger.warning(f"⚠️ Could not enable lazy install for {package_key}: {e}")
|
|
198
|
-
else:
|
|
199
|
-
try:
|
|
200
|
-
disable_lazy_install(package_key)
|
|
201
|
-
except ImportError:
|
|
202
|
-
pass
|
|
203
|
-
uninstall_import_hook(package_key)
|
|
204
|
-
cls._initialized[package_key] = False
|
|
205
|
-
_log("config", logger.info, f"❌ Lazy installation disabled for {package_key}")
|
|
206
|
-
sync_manifest_configuration(package_key)
|
|
207
|
-
|
|
208
|
-
@classmethod
|
|
209
|
-
def is_enabled(cls, package_name: str) -> bool:
|
|
210
|
-
"""Check if lazy installation is enabled for a package."""
|
|
211
|
-
return cls._configs.get(package_name.lower(), False)
|
|
212
|
-
|
|
213
|
-
@classmethod
|
|
214
|
-
def get_mode(cls, package_name: str) -> str:
|
|
215
|
-
"""Get the lazy installation mode for a package."""
|
|
216
|
-
return cls._modes.get(package_name.lower(), "auto")
|
|
217
|
-
|
|
218
|
-
@classmethod
|
|
219
|
-
def get_mode_config(cls, package_name: str) -> Optional[LazyModeConfig]:
|
|
220
|
-
"""Get the full mode configuration for a package."""
|
|
221
|
-
return cls._mode_configs.get(package_name.lower())
|
|
222
|
-
|
|
223
|
-
@classmethod
|
|
224
|
-
def get_load_mode(cls, package_name: str) -> LazyLoadMode:
|
|
225
|
-
"""Get the load mode for a package."""
|
|
226
|
-
return cls._load_modes.get(package_name.lower(), LazyLoadMode.NONE)
|
|
227
|
-
|
|
228
|
-
@classmethod
|
|
229
|
-
def get_install_mode(cls, package_name: str) -> LazyInstallMode:
|
|
230
|
-
"""Get the install mode for a package."""
|
|
231
|
-
return cls._install_modes.get(package_name.lower(), LazyInstallMode.NONE)
|
|
232
|
-
|
|
233
|
-
@classmethod
|
|
234
|
-
def set_install_mode(cls, package_name: str, mode: LazyInstallMode) -> None:
|
|
235
|
-
"""Set the install mode for a package."""
|
|
236
|
-
package_key = package_name.lower()
|
|
237
|
-
cls._install_modes[package_key] = mode
|
|
238
|
-
# Update mode config if it exists
|
|
239
|
-
if package_key in cls._mode_configs:
|
|
240
|
-
mode_config = cls._mode_configs[package_key]
|
|
241
|
-
cls._mode_configs[package_key] = LazyModeConfig(
|
|
242
|
-
load_mode=mode_config.load_mode,
|
|
243
|
-
install_mode=mode
|
|
244
|
-
)
|
|
245
|
-
|
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
#exonware/xwlazy/src/exonware/xwlazy/discovery/discovery.py
|
|
3
|
-
|
|
4
|
-
Package discovery implementation.
|
|
5
|
-
|
|
6
|
-
Company: eXonware.com
|
|
7
|
-
Author: Eng. Muhammad AlShehri
|
|
8
|
-
Email: connect@exonware.com
|
|
9
|
-
|
|
10
|
-
Generation Date: 10-Oct-2025
|
|
11
|
-
|
|
12
|
-
This module provides LazyDiscovery class that discovers dependencies from
|
|
13
|
-
project configuration sources with caching support.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import json
|
|
17
|
-
import re
|
|
18
|
-
import subprocess
|
|
19
|
-
import sys
|
|
20
|
-
import threading
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
from typing import Dict, List, Optional
|
|
23
|
-
|
|
24
|
-
from ..base import APackageHelper
|
|
25
|
-
from ...defs import DependencyInfo
|
|
26
|
-
from ...common.logger import get_logger, log_event as _log
|
|
27
|
-
|
|
28
|
-
logger = get_logger("xwlazy.discovery")
|
|
29
|
-
|
|
30
|
-
class LazyDiscovery(APackageHelper):
|
|
31
|
-
"""
|
|
32
|
-
Discovers dependencies from project configuration sources.
|
|
33
|
-
Implements caching with file modification time checks.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
# System/built-in modules that should NEVER be auto-installed
|
|
37
|
-
SYSTEM_MODULES_BLACKLIST = {
|
|
38
|
-
'pwd', 'grp', 'spwd', 'crypt', 'nis', 'syslog', 'termios', 'tty', 'pty',
|
|
39
|
-
'fcntl', 'resource', 'msvcrt', 'winreg', 'winsound', '_winapi',
|
|
40
|
-
'rpython', 'rply', 'rnc2rng', '_dbm',
|
|
41
|
-
'sys', 'os', 'io', 'time', 'datetime', 'json', 'csv', 'math',
|
|
42
|
-
'random', 're', 'collections', 'itertools', 'functools', 'operator',
|
|
43
|
-
'pathlib', 'shutil', 'glob', 'tempfile', 'pickle', 'copy', 'types',
|
|
44
|
-
'typing', 'abc', 'enum', 'dataclasses', 'contextlib', 'warnings',
|
|
45
|
-
'logging', 'threading', 'multiprocessing', 'subprocess', 'queue',
|
|
46
|
-
'socket', 'select', 'signal', 'asyncio', 'concurrent', 'email',
|
|
47
|
-
'http', 'urllib', 'xml', 'html', 'sqlite3', 'base64', 'hashlib',
|
|
48
|
-
'hmac', 'secrets', 'ssl', 'binascii', 'struct', 'array', 'weakref',
|
|
49
|
-
'gc', 'inspect', 'traceback', 'atexit', 'codecs', 'locale', 'gettext',
|
|
50
|
-
'argparse', 'optparse', 'configparser', 'fileinput', 'stat', 'platform',
|
|
51
|
-
'unittest', 'doctest', 'pdb', 'profile', 'cProfile', 'timeit', 'trace',
|
|
52
|
-
# Internal / optional modules that must never trigger auto-install
|
|
53
|
-
'compression', 'socks', 'wimlib',
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# Common import name to package name mappings
|
|
57
|
-
COMMON_MAPPINGS = {
|
|
58
|
-
'cv2': 'opencv-python',
|
|
59
|
-
'PIL': 'Pillow',
|
|
60
|
-
'Pillow': 'Pillow',
|
|
61
|
-
'yaml': 'PyYAML',
|
|
62
|
-
'sklearn': 'scikit-learn',
|
|
63
|
-
'bs4': 'beautifulsoup4',
|
|
64
|
-
'dateutil': 'python-dateutil',
|
|
65
|
-
'requests_oauthlib': 'requests-oauthlib',
|
|
66
|
-
'google': 'google-api-python-client',
|
|
67
|
-
'jwt': 'PyJWT',
|
|
68
|
-
'crypto': 'pycrypto',
|
|
69
|
-
'Crypto': 'pycrypto',
|
|
70
|
-
'MySQLdb': 'mysqlclient',
|
|
71
|
-
'psycopg2': 'psycopg2-binary',
|
|
72
|
-
'bson': 'pymongo',
|
|
73
|
-
'lxml': 'lxml',
|
|
74
|
-
'numpy': 'numpy',
|
|
75
|
-
'pandas': 'pandas',
|
|
76
|
-
'matplotlib': 'matplotlib',
|
|
77
|
-
'seaborn': 'seaborn',
|
|
78
|
-
'plotly': 'plotly',
|
|
79
|
-
'django': 'Django',
|
|
80
|
-
'flask': 'Flask',
|
|
81
|
-
'fastapi': 'fastapi',
|
|
82
|
-
'uvicorn': 'uvicorn',
|
|
83
|
-
'pytest': 'pytest',
|
|
84
|
-
'black': 'black',
|
|
85
|
-
'isort': 'isort',
|
|
86
|
-
'mypy': 'mypy',
|
|
87
|
-
'psutil': 'psutil',
|
|
88
|
-
'colorama': 'colorama',
|
|
89
|
-
'pytz': 'pytz',
|
|
90
|
-
'aiofiles': 'aiofiles',
|
|
91
|
-
'watchdog': 'watchdog',
|
|
92
|
-
'wand': 'Wand',
|
|
93
|
-
'exifread': 'ExifRead',
|
|
94
|
-
'piexif': 'piexif',
|
|
95
|
-
'rawpy': 'rawpy',
|
|
96
|
-
'imageio': 'imageio',
|
|
97
|
-
'scipy': 'scipy',
|
|
98
|
-
'scikit-image': 'scikit-image',
|
|
99
|
-
'opencv-python': 'opencv-python',
|
|
100
|
-
'opencv-contrib-python': 'opencv-contrib-python',
|
|
101
|
-
'opentelemetry': 'opentelemetry-api',
|
|
102
|
-
'opentelemetry.trace': 'opentelemetry-api',
|
|
103
|
-
'opentelemetry.sdk': 'opentelemetry-sdk',
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
def _discover_from_sources(self) -> None:
|
|
107
|
-
"""Discover dependencies from all sources."""
|
|
108
|
-
self._discover_from_pyproject_toml()
|
|
109
|
-
self._discover_from_requirements_txt()
|
|
110
|
-
self._discover_from_setup_py()
|
|
111
|
-
self._discover_from_custom_config()
|
|
112
|
-
self._add_common_mappings() # Add well-known mappings (bson->pymongo, cv2->opencv-python, etc.)
|
|
113
|
-
|
|
114
|
-
def _is_cache_valid(self) -> bool:
|
|
115
|
-
"""Check if cached dependencies are still valid."""
|
|
116
|
-
if not self._cache_valid or not self._cached_dependencies:
|
|
117
|
-
return False
|
|
118
|
-
|
|
119
|
-
config_files = [
|
|
120
|
-
self.project_root / 'pyproject.toml',
|
|
121
|
-
self.project_root / 'requirements.txt',
|
|
122
|
-
self.project_root / 'setup.py',
|
|
123
|
-
]
|
|
124
|
-
|
|
125
|
-
for config_file in config_files:
|
|
126
|
-
if config_file.exists():
|
|
127
|
-
try:
|
|
128
|
-
current_mtime = config_file.stat().st_mtime
|
|
129
|
-
cached_mtime = self._file_mtimes.get(str(config_file), 0)
|
|
130
|
-
if current_mtime > cached_mtime:
|
|
131
|
-
return False
|
|
132
|
-
except Exception:
|
|
133
|
-
return False
|
|
134
|
-
|
|
135
|
-
return True
|
|
136
|
-
|
|
137
|
-
def _update_file_mtimes(self) -> None:
|
|
138
|
-
"""Update file modification times for cache validation."""
|
|
139
|
-
config_files = [
|
|
140
|
-
self.project_root / 'pyproject.toml',
|
|
141
|
-
self.project_root / 'requirements.txt',
|
|
142
|
-
self.project_root / 'setup.py',
|
|
143
|
-
]
|
|
144
|
-
for config_file in config_files:
|
|
145
|
-
if config_file.exists():
|
|
146
|
-
try:
|
|
147
|
-
self._file_mtimes[str(config_file)] = config_file.stat().st_mtime
|
|
148
|
-
except Exception:
|
|
149
|
-
pass
|
|
150
|
-
|
|
151
|
-
def _discover_from_pyproject_toml(self) -> None:
|
|
152
|
-
"""Discover dependencies from pyproject.toml."""
|
|
153
|
-
pyproject_path = self.project_root / 'pyproject.toml'
|
|
154
|
-
if not pyproject_path.exists():
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
try:
|
|
158
|
-
try:
|
|
159
|
-
import tomllib # Python 3.11+
|
|
160
|
-
toml_parser = tomllib # type: ignore[assignment]
|
|
161
|
-
except ImportError:
|
|
162
|
-
try:
|
|
163
|
-
import tomli as tomllib # type: ignore[assignment]
|
|
164
|
-
toml_parser = tomllib
|
|
165
|
-
except ImportError:
|
|
166
|
-
_log(
|
|
167
|
-
"discovery",
|
|
168
|
-
"TOML parser not available; attempting to lazy-install 'tomli'...",
|
|
169
|
-
)
|
|
170
|
-
try:
|
|
171
|
-
subprocess.run(
|
|
172
|
-
[sys.executable, "-m", "pip", "install", "tomli"],
|
|
173
|
-
check=False,
|
|
174
|
-
capture_output=True,
|
|
175
|
-
)
|
|
176
|
-
import tomli as tomllib # type: ignore[assignment]
|
|
177
|
-
toml_parser = tomllib
|
|
178
|
-
except Exception as install_exc:
|
|
179
|
-
logger.warning(
|
|
180
|
-
"tomli installation failed; skipping pyproject.toml discovery "
|
|
181
|
-
f"({install_exc})"
|
|
182
|
-
)
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
with open(pyproject_path, 'rb') as f:
|
|
186
|
-
data = toml_parser.load(f)
|
|
187
|
-
|
|
188
|
-
dependencies = []
|
|
189
|
-
if 'project' in data and 'dependencies' in data['project']:
|
|
190
|
-
dependencies.extend(data['project']['dependencies'])
|
|
191
|
-
|
|
192
|
-
if 'project' in data and 'optional-dependencies' in data['project']:
|
|
193
|
-
for group_name, group_deps in data['project']['optional-dependencies'].items():
|
|
194
|
-
dependencies.extend(group_deps)
|
|
195
|
-
|
|
196
|
-
if 'build-system' in data and 'requires' in data['build-system']:
|
|
197
|
-
dependencies.extend(data['build-system']['requires'])
|
|
198
|
-
|
|
199
|
-
for dep in dependencies:
|
|
200
|
-
self._parse_dependency_string(dep, 'pyproject.toml')
|
|
201
|
-
|
|
202
|
-
self._discovery_sources.append('pyproject.toml')
|
|
203
|
-
except Exception as e:
|
|
204
|
-
logger.warning(f"Could not parse pyproject.toml: {e}")
|
|
205
|
-
|
|
206
|
-
def _discover_from_requirements_txt(self) -> None:
|
|
207
|
-
"""Discover dependencies from requirements.txt."""
|
|
208
|
-
requirements_path = self.project_root / 'requirements.txt'
|
|
209
|
-
if not requirements_path.exists():
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
try:
|
|
213
|
-
with open(requirements_path, 'r', encoding='utf-8') as f:
|
|
214
|
-
for line in f:
|
|
215
|
-
line = line.strip()
|
|
216
|
-
if line and not line.startswith('#'):
|
|
217
|
-
self._parse_dependency_string(line, 'requirements.txt')
|
|
218
|
-
|
|
219
|
-
self._discovery_sources.append('requirements.txt')
|
|
220
|
-
except Exception as e:
|
|
221
|
-
logger.warning(f"Could not parse requirements.txt: {e}")
|
|
222
|
-
|
|
223
|
-
def _discover_from_setup_py(self) -> None:
|
|
224
|
-
"""Discover dependencies from setup.py."""
|
|
225
|
-
setup_path = self.project_root / 'setup.py'
|
|
226
|
-
if not setup_path.exists():
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
try:
|
|
230
|
-
with open(setup_path, 'r', encoding='utf-8') as f:
|
|
231
|
-
content = f.read()
|
|
232
|
-
|
|
233
|
-
install_requires_match = re.search(
|
|
234
|
-
r'install_requires\s*=\s*\[(.*?)\]',
|
|
235
|
-
content,
|
|
236
|
-
re.DOTALL
|
|
237
|
-
)
|
|
238
|
-
if install_requires_match:
|
|
239
|
-
deps_str = install_requires_match.group(1)
|
|
240
|
-
deps = re.findall(r'["\']([^"\']+)["\']', deps_str)
|
|
241
|
-
for dep in deps:
|
|
242
|
-
self._parse_dependency_string(dep, 'setup.py')
|
|
243
|
-
|
|
244
|
-
self._discovery_sources.append('setup.py')
|
|
245
|
-
except Exception as e:
|
|
246
|
-
logger.warning(f"Could not parse setup.py: {e}")
|
|
247
|
-
|
|
248
|
-
def _discover_from_custom_config(self) -> None:
|
|
249
|
-
"""Discover dependencies from custom configuration files."""
|
|
250
|
-
config_files = [
|
|
251
|
-
'dependency-mappings.json',
|
|
252
|
-
'lazy-dependencies.json',
|
|
253
|
-
'dependencies.json'
|
|
254
|
-
]
|
|
255
|
-
|
|
256
|
-
for config_file in config_files:
|
|
257
|
-
config_path = self.project_root / config_file
|
|
258
|
-
if config_path.exists():
|
|
259
|
-
try:
|
|
260
|
-
with open(config_path, 'r', encoding='utf-8') as f:
|
|
261
|
-
data = json.load(f)
|
|
262
|
-
|
|
263
|
-
if isinstance(data, dict):
|
|
264
|
-
for import_name, package_name in data.items():
|
|
265
|
-
self.discovered_dependencies[import_name] = DependencyInfo(
|
|
266
|
-
import_name=import_name,
|
|
267
|
-
package_name=package_name,
|
|
268
|
-
source=config_file,
|
|
269
|
-
category='custom'
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
self._discovery_sources.append(config_file)
|
|
273
|
-
except Exception as e:
|
|
274
|
-
logger.warning(f"Could not parse {config_file}: {e}")
|
|
275
|
-
|
|
276
|
-
def _parse_dependency_string(self, dep_str: str, source: str) -> None:
|
|
277
|
-
"""Parse a dependency string and extract dependency information."""
|
|
278
|
-
dep_str = re.sub(r'[>=<!=~]+.*', '', dep_str)
|
|
279
|
-
dep_str = re.sub(r'\[.*\]', '', dep_str)
|
|
280
|
-
dep_str = dep_str.strip()
|
|
281
|
-
|
|
282
|
-
if not dep_str:
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
import_name = dep_str
|
|
286
|
-
package_name = dep_str
|
|
287
|
-
|
|
288
|
-
if dep_str in self.COMMON_MAPPINGS:
|
|
289
|
-
package_name = self.COMMON_MAPPINGS[dep_str]
|
|
290
|
-
elif dep_str in self.COMMON_MAPPINGS.values():
|
|
291
|
-
for imp_name, pkg_name in self.COMMON_MAPPINGS.items():
|
|
292
|
-
if pkg_name == dep_str:
|
|
293
|
-
import_name = imp_name
|
|
294
|
-
break
|
|
295
|
-
|
|
296
|
-
self.discovered_dependencies[import_name] = DependencyInfo(
|
|
297
|
-
import_name=import_name,
|
|
298
|
-
package_name=package_name,
|
|
299
|
-
source=source,
|
|
300
|
-
category='discovered'
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
def _add_common_mappings(self) -> None:
|
|
304
|
-
"""Add common mappings that might not be in dependency files."""
|
|
305
|
-
for import_name, package_name in self.COMMON_MAPPINGS.items():
|
|
306
|
-
if import_name not in self.discovered_dependencies:
|
|
307
|
-
self.discovered_dependencies[import_name] = DependencyInfo(
|
|
308
|
-
import_name=import_name,
|
|
309
|
-
package_name=package_name,
|
|
310
|
-
source='common_mappings',
|
|
311
|
-
category='common'
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
def get_package_for_import(self, import_name: str) -> Optional[str]:
|
|
315
|
-
"""Get package name for a given import name."""
|
|
316
|
-
mapping = self.discover_all_dependencies()
|
|
317
|
-
return mapping.get(import_name)
|
|
318
|
-
|
|
319
|
-
def get_imports_for_package(self, package_name: str) -> List[str]:
|
|
320
|
-
"""Get all possible import names for a package."""
|
|
321
|
-
mapping = self.get_package_import_mapping()
|
|
322
|
-
return mapping.get(package_name, [package_name])
|
|
323
|
-
|
|
324
|
-
def get_package_import_mapping(self) -> Dict[str, List[str]]:
|
|
325
|
-
"""Get mapping of package names to their possible import names."""
|
|
326
|
-
self.discover_all_dependencies()
|
|
327
|
-
|
|
328
|
-
package_to_imports = {}
|
|
329
|
-
for import_name, dep_info in self.discovered_dependencies.items():
|
|
330
|
-
package_name = dep_info.package_name
|
|
331
|
-
|
|
332
|
-
if package_name not in package_to_imports:
|
|
333
|
-
package_to_imports[package_name] = [package_name]
|
|
334
|
-
|
|
335
|
-
if import_name != package_name:
|
|
336
|
-
if import_name not in package_to_imports[package_name]:
|
|
337
|
-
package_to_imports[package_name].append(import_name)
|
|
338
|
-
|
|
339
|
-
return package_to_imports
|
|
340
|
-
|
|
341
|
-
def get_import_package_mapping(self) -> Dict[str, str]:
|
|
342
|
-
"""Get mapping of import names to package names."""
|
|
343
|
-
self.discover_all_dependencies()
|
|
344
|
-
return {import_name: dep_info.package_name for import_name, dep_info in self.discovered_dependencies.items()}
|
|
345
|
-
|
|
346
|
-
def export_to_json(self, file_path: str) -> None:
|
|
347
|
-
"""Export discovered dependencies to JSON file."""
|
|
348
|
-
data = {
|
|
349
|
-
'dependencies': {name: info.package_name for name, info in self.discovered_dependencies.items()},
|
|
350
|
-
'sources': self.get_discovery_sources(),
|
|
351
|
-
'total_count': len(self.discovered_dependencies)
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
with open(file_path, 'w', encoding='utf-8') as f:
|
|
355
|
-
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
356
|
-
|
|
357
|
-
# Global discovery instance
|
|
358
|
-
_discovery: Optional[LazyDiscovery] = None
|
|
359
|
-
_discovery_lock = threading.RLock()
|
|
360
|
-
|
|
361
|
-
def get_lazy_discovery(project_root: Optional[str] = None) -> LazyDiscovery:
|
|
362
|
-
"""Get or create global discovery instance."""
|
|
363
|
-
global _discovery
|
|
364
|
-
with _discovery_lock:
|
|
365
|
-
if _discovery is None:
|
|
366
|
-
_discovery = LazyDiscovery(project_root)
|
|
367
|
-
return _discovery
|
|
368
|
-
|
|
369
|
-
__all__ = ['LazyDiscovery', 'get_lazy_discovery']
|
|
370
|
-
|