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,158 @@
1
+ """
2
+ Install Policy
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
+ Security and policy configuration for lazy installation.
11
+ """
12
+
13
+ import threading
14
+ from typing import Dict, List, Optional, Set, Tuple
15
+
16
+ # Lazy import to avoid circular dependency
17
+ def _get_log_event():
18
+ """Get log_event function (lazy import to avoid circular dependency)."""
19
+ from ...common.logger import log_event
20
+ return log_event
21
+
22
+ _log = None # Will be initialized on first use
23
+
24
+
25
+ class LazyInstallPolicy:
26
+ """
27
+ Security and policy configuration for lazy installation.
28
+ Per-package allow/deny lists, index URLs, and security settings.
29
+ """
30
+ __slots__ = ()
31
+
32
+ _allow_lists: Dict[str, Set[str]] = {}
33
+ _deny_lists: Dict[str, Set[str]] = {}
34
+ _index_urls: Dict[str, str] = {}
35
+ _extra_index_urls: Dict[str, List[str]] = {}
36
+ _trusted_hosts: Dict[str, List[str]] = {}
37
+ _require_hashes: Dict[str, bool] = {}
38
+ _verify_ssl: Dict[str, bool] = {}
39
+ _lockfile_paths: Dict[str, str] = {}
40
+ _lock = threading.RLock()
41
+
42
+ @classmethod
43
+ def _ensure_logging(cls):
44
+ """Ensure logging is initialized."""
45
+ global _log
46
+ if _log is None:
47
+ _log = _get_log_event()
48
+
49
+ @classmethod
50
+ def set_allow_list(cls, package_name: str, allowed_packages: List[str]) -> None:
51
+ """Set allow list for a package (only these can be installed)."""
52
+ cls._ensure_logging()
53
+ with cls._lock:
54
+ cls._allow_lists[package_name] = set(allowed_packages)
55
+ _log("config", f"Set allow list for {package_name}: {len(allowed_packages)} packages")
56
+
57
+ @classmethod
58
+ def set_deny_list(cls, package_name: str, denied_packages: List[str]) -> None:
59
+ """Set deny list for a package (these cannot be installed)."""
60
+ cls._ensure_logging()
61
+ with cls._lock:
62
+ cls._deny_lists[package_name] = set(denied_packages)
63
+ _log("config", f"Set deny list for {package_name}: {len(denied_packages)} packages")
64
+
65
+ @classmethod
66
+ def add_to_allow_list(cls, package_name: str, allowed_package: str) -> None:
67
+ """Add single package to allow list."""
68
+ with cls._lock:
69
+ if package_name not in cls._allow_lists:
70
+ cls._allow_lists[package_name] = set()
71
+ cls._allow_lists[package_name].add(allowed_package)
72
+
73
+ @classmethod
74
+ def add_to_deny_list(cls, package_name: str, denied_package: str) -> None:
75
+ """Add single package to deny list."""
76
+ with cls._lock:
77
+ if package_name not in cls._deny_lists:
78
+ cls._deny_lists[package_name] = set()
79
+ cls._deny_lists[package_name].add(denied_package)
80
+
81
+ @classmethod
82
+ def is_package_allowed(cls, installer_package: str, target_package: str) -> Tuple[bool, str]:
83
+ """Check if target_package can be installed by installer_package."""
84
+ with cls._lock:
85
+ if installer_package in cls._deny_lists:
86
+ if target_package in cls._deny_lists[installer_package]:
87
+ return False, f"Package '{target_package}' is in deny list"
88
+
89
+ if installer_package in cls._allow_lists:
90
+ if target_package not in cls._allow_lists[installer_package]:
91
+ return False, f"Package '{target_package}' not in allow list"
92
+
93
+ return True, "OK"
94
+
95
+ @classmethod
96
+ def set_index_url(cls, package_name: str, index_url: str) -> None:
97
+ """Set PyPI index URL for a package."""
98
+ cls._ensure_logging()
99
+ with cls._lock:
100
+ cls._index_urls[package_name] = index_url
101
+ _log("config", f"Set index URL for {package_name}: {index_url}")
102
+
103
+ @classmethod
104
+ def set_extra_index_urls(cls, package_name: str, urls: List[str]) -> None:
105
+ """Set extra index URLs for a package."""
106
+ cls._ensure_logging()
107
+ with cls._lock:
108
+ cls._extra_index_urls[package_name] = urls
109
+ _log("config", f"Set {len(urls)} extra index URLs for {package_name}")
110
+
111
+ @classmethod
112
+ def add_trusted_host(cls, package_name: str, host: str) -> None:
113
+ """Add trusted host for a package."""
114
+ with cls._lock:
115
+ if package_name not in cls._trusted_hosts:
116
+ cls._trusted_hosts[package_name] = []
117
+ cls._trusted_hosts[package_name].append(host)
118
+
119
+ @classmethod
120
+ def get_pip_args(cls, package_name: str) -> List[str]:
121
+ """Get pip install arguments for a package based on policy."""
122
+ args = []
123
+
124
+ with cls._lock:
125
+ if package_name in cls._index_urls:
126
+ args.extend(['--index-url', cls._index_urls[package_name]])
127
+
128
+ if package_name in cls._extra_index_urls:
129
+ for url in cls._extra_index_urls[package_name]:
130
+ args.extend(['--extra-index-url', url])
131
+
132
+ if package_name in cls._trusted_hosts:
133
+ for host in cls._trusted_hosts[package_name]:
134
+ args.extend(['--trusted-host', host])
135
+
136
+ if cls._require_hashes.get(package_name, False):
137
+ args.append('--require-hashes')
138
+
139
+ if not cls._verify_ssl.get(package_name, True):
140
+ args.append('--no-verify-ssl')
141
+
142
+ return args
143
+
144
+ @classmethod
145
+ def set_lockfile_path(cls, package_name: str, path: str) -> None:
146
+ """Set lockfile path for a package."""
147
+ with cls._lock:
148
+ cls._lockfile_paths[package_name] = path
149
+
150
+ @classmethod
151
+ def get_lockfile_path(cls, package_name: str) -> Optional[str]:
152
+ """Get lockfile path for a package."""
153
+ with cls._lock:
154
+ return cls._lockfile_paths.get(package_name)
155
+
156
+
157
+ __all__ = ['LazyInstallPolicy']
158
+
@@ -0,0 +1,56 @@
1
+ """
2
+ Installer Registry
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
+ Registry to manage separate lazy installer instances per package.
11
+ """
12
+
13
+ import threading
14
+ from typing import Dict, TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from .lazy_installer import LazyInstaller
18
+
19
+
20
+ class LazyInstallerRegistry:
21
+ """Registry to manage separate lazy installer instances per package."""
22
+ _instances: Dict[str, 'LazyInstaller'] = {}
23
+ _lock = threading.RLock()
24
+
25
+ @classmethod
26
+ def get_instance(cls, package_name: str = 'default') -> 'LazyInstaller':
27
+ """
28
+ Get or create a lazy installer instance for a package.
29
+
30
+ Args:
31
+ package_name: Package name for isolation
32
+
33
+ Returns:
34
+ LazyInstaller instance for the package
35
+ """
36
+ with cls._lock:
37
+ if package_name not in cls._instances:
38
+ # Lazy import to avoid circular dependency
39
+ from .lazy_installer import LazyInstaller
40
+ cls._instances[package_name] = LazyInstaller(package_name)
41
+ return cls._instances[package_name]
42
+
43
+ @classmethod
44
+ def get_all_instances(cls) -> Dict[str, 'LazyInstaller']:
45
+ """
46
+ Get all lazy installer instances.
47
+
48
+ Returns:
49
+ Dict mapping package_name -> LazyInstaller
50
+ """
51
+ with cls._lock:
52
+ return cls._instances.copy()
53
+
54
+
55
+ __all__ = ['LazyInstallerRegistry']
56
+
@@ -0,0 +1,17 @@
1
+ """
2
+ Install Result and Status
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
+ Re-export installation result types from defs.py for backward compatibility.
11
+ """
12
+
13
+ # Re-export from defs.py
14
+ from ...defs import InstallStatus, InstallResult
15
+
16
+ __all__ = ['InstallStatus', 'InstallResult']
17
+
@@ -0,0 +1,154 @@
1
+ """
2
+ SBOM and Audit Mixin
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
+ Mixin for SBOM generation and vulnerability auditing.
11
+ """
12
+
13
+ import json
14
+ import sys
15
+ import subprocess
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import Dict
19
+
20
+ from .install_policy import LazyInstallPolicy
21
+
22
+ # Lazy imports
23
+ def _get_logger():
24
+ """Get logger (lazy import to avoid circular dependency)."""
25
+ from ...common.logger import get_logger
26
+ return get_logger("xwlazy.lazy_installer")
27
+
28
+ def _get_log_event():
29
+ """Get log_event function (lazy import to avoid circular dependency)."""
30
+ from ...common.logger import log_event
31
+ return log_event
32
+
33
+ logger = None
34
+ _log = None
35
+
36
+ def _ensure_logging_initialized():
37
+ """Ensure logging utilities are initialized."""
38
+ global logger, _log
39
+ if logger is None:
40
+ logger = _get_logger()
41
+ if _log is None:
42
+ _log = _get_log_event()
43
+
44
+
45
+ class SBOMAuditMixin:
46
+ """Mixin for SBOM generation and vulnerability auditing."""
47
+
48
+ def _run_vulnerability_audit(self, package_name: str) -> None:
49
+ """Run vulnerability audit on installed package using pip-audit."""
50
+ _ensure_logging_initialized()
51
+ try:
52
+ result = subprocess.run(
53
+ [sys.executable, '-m', 'pip_audit', '-r', '-', '--format', 'json'],
54
+ input=package_name,
55
+ capture_output=True,
56
+ text=True,
57
+ timeout=30
58
+ )
59
+
60
+ if result.returncode == 0:
61
+ _log("audit", f"Vulnerability audit passed for {package_name}")
62
+ else:
63
+ try:
64
+ audit_data = json.loads(result.stdout)
65
+ if audit_data.get('vulnerabilities'):
66
+ logger.warning(f"[SECURITY] Vulnerabilities found in {package_name}: {audit_data}")
67
+ print(f"[SECURITY WARNING] Package '{package_name}' has known vulnerabilities")
68
+ print(f"Run 'pip-audit' for details")
69
+ except json.JSONDecodeError:
70
+ logger.warning(f"Could not parse audit results for {package_name}")
71
+ except subprocess.TimeoutExpired:
72
+ logger.warning(f"Vulnerability audit timed out for {package_name}")
73
+ except Exception as e:
74
+ logger.debug(f"Vulnerability audit skipped for {package_name}: {e}")
75
+
76
+ def _update_lockfile(self, package_name: str) -> None:
77
+ """Update lockfile with newly installed package."""
78
+ _ensure_logging_initialized()
79
+ lockfile_path = LazyInstallPolicy.get_lockfile_path(self._package_name) # type: ignore[attr-defined]
80
+ if not lockfile_path:
81
+ return
82
+
83
+ try:
84
+ version = self._get_installed_version(package_name) # type: ignore[attr-defined]
85
+ if not version:
86
+ return
87
+
88
+ lockfile_path = Path(lockfile_path)
89
+ if lockfile_path.exists():
90
+ with open(lockfile_path, 'r', encoding='utf-8') as f:
91
+ lockdata = json.load(f)
92
+ else:
93
+ lockdata = {
94
+ "metadata": {
95
+ "generated_by": f"xwlazy-{self._package_name}", # type: ignore[attr-defined]
96
+ "version": "1.0"
97
+ },
98
+ "packages": {}
99
+ }
100
+
101
+ lockdata["packages"][package_name] = {
102
+ "version": version,
103
+ "installed_at": datetime.now().isoformat(),
104
+ "installer": self._package_name # type: ignore[attr-defined]
105
+ }
106
+
107
+ lockfile_path.parent.mkdir(parents=True, exist_ok=True)
108
+ with open(lockfile_path, 'w', encoding='utf-8') as f:
109
+ json.dump(lockdata, f, indent=2)
110
+
111
+ _log("sbom", f"Updated lockfile: {lockfile_path}")
112
+ except Exception as e:
113
+ logger.warning(f"Failed to update lockfile: {e}")
114
+
115
+ def generate_sbom(self) -> Dict:
116
+ """Generate Software Bill of Materials (SBOM) for installed packages."""
117
+ sbom = {
118
+ "metadata": {
119
+ "format": "xwlazy-sbom",
120
+ "version": "1.0",
121
+ "generated_at": datetime.now().isoformat(),
122
+ "installer_package": self._package_name # type: ignore[attr-defined]
123
+ },
124
+ "packages": []
125
+ }
126
+
127
+ for pkg in self._installed_packages: # type: ignore[attr-defined]
128
+ version = self._get_installed_version(pkg) # type: ignore[attr-defined]
129
+ sbom["packages"].append({
130
+ "name": pkg,
131
+ "version": version or "unknown",
132
+ "installed_by": self._package_name, # type: ignore[attr-defined]
133
+ "source": "pypi"
134
+ })
135
+
136
+ return sbom
137
+
138
+ def export_sbom(self, output_path: str) -> bool:
139
+ """Export SBOM to file."""
140
+ _ensure_logging_initialized()
141
+ try:
142
+ sbom = self.generate_sbom()
143
+ output_path = Path(output_path)
144
+ output_path.parent.mkdir(parents=True, exist_ok=True)
145
+
146
+ with open(output_path, 'w', encoding='utf-8') as f:
147
+ json.dump(sbom, f, indent=2)
148
+
149
+ _log("sbom", f"Exported SBOM to: {output_path}")
150
+ return True
151
+ except Exception as e:
152
+ logger.error(f"Failed to export SBOM: {e}")
153
+ return False
154
+
@@ -0,0 +1,83 @@
1
+ """
2
+ Installation 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
+ Utility functions for installation operations.
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ import inspect
16
+ import subprocess
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+
21
+ def get_trigger_file() -> Optional[str]:
22
+ """
23
+ Get the file that triggered the import (from call stack).
24
+
25
+ Returns:
26
+ Filename that triggered the import, or None
27
+ """
28
+ try:
29
+ # Walk up the call stack to find the first non-xwlazy file
30
+ # Look for files in xwsystem, xwnode, xwdata, or user code
31
+ for frame_info in inspect.stack():
32
+ filename = frame_info.filename
33
+ # Skip xwlazy internal files and importlib
34
+ if ('xwlazy' not in filename and
35
+ 'importlib' not in filename and
36
+ '<frozen' not in filename and
37
+ filename.endswith('.py')):
38
+ # Return just the filename, not full path
39
+ basename = os.path.basename(filename)
40
+ # If it's a serialization file, use that
41
+ if 'serialization' in filename or 'formats' in filename:
42
+ # Extract the format name (e.g., bson.py -> BsonSerializer)
43
+ if basename.endswith('.py'):
44
+ basename = basename[:-3] # Remove .py
45
+ return f"{basename.capitalize()}Serializer" if basename else None
46
+ return basename
47
+ except Exception:
48
+ pass
49
+ return None
50
+
51
+
52
+ def is_externally_managed() -> bool:
53
+ """
54
+ Check if Python environment is externally managed (PEP 668).
55
+
56
+ Returns:
57
+ True if environment is externally managed
58
+ """
59
+ marker_file = Path(sys.prefix) / "EXTERNALLY-MANAGED"
60
+ return marker_file.exists()
61
+
62
+
63
+ def check_pip_audit_available() -> bool:
64
+ """
65
+ Check if pip-audit is available for vulnerability scanning.
66
+
67
+ Returns:
68
+ True if pip-audit is available
69
+ """
70
+ try:
71
+ result = subprocess.run(
72
+ [sys.executable, '-m', 'pip', 'list'],
73
+ capture_output=True,
74
+ text=True,
75
+ timeout=5
76
+ )
77
+ return 'pip-audit' in result.stdout
78
+ except Exception:
79
+ return False
80
+
81
+
82
+ __all__ = ['get_trigger_file', 'is_externally_managed', 'check_pip_audit_available']
83
+