exonware-xwlazy 0.1.0.11__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 (96) 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/strategies/__init__.py +28 -0
  14. exonware/xwlazy/common/strategies/caching_dict.py +45 -0
  15. exonware/xwlazy/common/strategies/caching_installation.py +89 -0
  16. exonware/xwlazy/common/strategies/caching_lfu.py +67 -0
  17. exonware/xwlazy/common/strategies/caching_lru.py +64 -0
  18. exonware/xwlazy/common/strategies/caching_multitier.py +60 -0
  19. exonware/xwlazy/common/strategies/caching_ttl.py +60 -0
  20. {xwlazy/lazy → exonware/xwlazy}/config.py +52 -20
  21. exonware/xwlazy/contracts.py +1410 -0
  22. exonware/xwlazy/defs.py +397 -0
  23. xwlazy/lazy/lazy_errors.py → exonware/xwlazy/errors.py +21 -8
  24. exonware/xwlazy/facade.py +1049 -0
  25. exonware/xwlazy/module/__init__.py +18 -0
  26. exonware/xwlazy/module/base.py +569 -0
  27. exonware/xwlazy/module/data.py +17 -0
  28. exonware/xwlazy/module/facade.py +247 -0
  29. exonware/xwlazy/module/importer_engine.py +2161 -0
  30. exonware/xwlazy/module/strategies/__init__.py +22 -0
  31. exonware/xwlazy/module/strategies/module_helper_lazy.py +94 -0
  32. exonware/xwlazy/module/strategies/module_helper_simple.py +66 -0
  33. exonware/xwlazy/module/strategies/module_manager_advanced.py +112 -0
  34. exonware/xwlazy/module/strategies/module_manager_simple.py +96 -0
  35. exonware/xwlazy/package/__init__.py +18 -0
  36. exonware/xwlazy/package/base.py +807 -0
  37. xwlazy/lazy/host_conf.py → exonware/xwlazy/package/conf.py +62 -10
  38. exonware/xwlazy/package/data.py +17 -0
  39. exonware/xwlazy/package/facade.py +481 -0
  40. exonware/xwlazy/package/services/__init__.py +84 -0
  41. exonware/xwlazy/package/services/async_install_handle.py +89 -0
  42. exonware/xwlazy/package/services/config_manager.py +246 -0
  43. exonware/xwlazy/package/services/discovery.py +374 -0
  44. {xwlazy/lazy → exonware/xwlazy/package/services}/host_packages.py +43 -16
  45. exonware/xwlazy/package/services/install_async.py +278 -0
  46. exonware/xwlazy/package/services/install_cache.py +146 -0
  47. exonware/xwlazy/package/services/install_interactive.py +60 -0
  48. exonware/xwlazy/package/services/install_policy.py +158 -0
  49. exonware/xwlazy/package/services/install_registry.py +56 -0
  50. exonware/xwlazy/package/services/install_result.py +17 -0
  51. exonware/xwlazy/package/services/install_sbom.py +154 -0
  52. exonware/xwlazy/package/services/install_utils.py +83 -0
  53. exonware/xwlazy/package/services/installer_engine.py +408 -0
  54. exonware/xwlazy/package/services/lazy_installer.py +720 -0
  55. {xwlazy/lazy → exonware/xwlazy/package/services}/manifest.py +42 -25
  56. exonware/xwlazy/package/services/strategy_registry.py +188 -0
  57. exonware/xwlazy/package/strategies/__init__.py +57 -0
  58. exonware/xwlazy/package/strategies/package_discovery_file.py +130 -0
  59. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +85 -0
  60. exonware/xwlazy/package/strategies/package_discovery_manifest.py +102 -0
  61. exonware/xwlazy/package/strategies/package_execution_async.py +114 -0
  62. exonware/xwlazy/package/strategies/package_execution_cached.py +91 -0
  63. exonware/xwlazy/package/strategies/package_execution_pip.py +100 -0
  64. exonware/xwlazy/package/strategies/package_execution_wheel.py +107 -0
  65. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +101 -0
  66. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +106 -0
  67. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +101 -0
  68. exonware/xwlazy/package/strategies/package_policy_allow_list.py +58 -0
  69. exonware/xwlazy/package/strategies/package_policy_deny_list.py +58 -0
  70. exonware/xwlazy/package/strategies/package_policy_permissive.py +47 -0
  71. exonware/xwlazy/package/strategies/package_timing_clean.py +68 -0
  72. exonware/xwlazy/package/strategies/package_timing_full.py +67 -0
  73. exonware/xwlazy/package/strategies/package_timing_smart.py +69 -0
  74. exonware/xwlazy/package/strategies/package_timing_temporary.py +67 -0
  75. exonware/xwlazy/runtime/__init__.py +18 -0
  76. exonware/xwlazy/runtime/adaptive_learner.py +131 -0
  77. exonware/xwlazy/runtime/base.py +276 -0
  78. exonware/xwlazy/runtime/facade.py +95 -0
  79. exonware/xwlazy/runtime/intelligent_selector.py +173 -0
  80. exonware/xwlazy/runtime/metrics.py +64 -0
  81. exonware/xwlazy/runtime/performance.py +39 -0
  82. exonware/xwlazy/version.py +2 -2
  83. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/METADATA +86 -10
  84. exonware_xwlazy-0.1.0.19.dist-info/RECORD +87 -0
  85. exonware_xwlazy-0.1.0.11.dist-info/RECORD +0 -20
  86. xwlazy/__init__.py +0 -34
  87. xwlazy/lazy/__init__.py +0 -301
  88. xwlazy/lazy/bootstrap.py +0 -106
  89. xwlazy/lazy/lazy_base.py +0 -465
  90. xwlazy/lazy/lazy_contracts.py +0 -290
  91. xwlazy/lazy/lazy_core.py +0 -3727
  92. xwlazy/lazy/logging_utils.py +0 -194
  93. xwlazy/version.py +0 -77
  94. /xwlazy/lazy/lazy_state.py → /exonware/xwlazy/common/services/state_manager.py +0 -0
  95. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/WHEEL +0 -0
  96. {exonware_xwlazy-0.1.0.11.dist-info → exonware_xwlazy-0.1.0.19.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  """
4
- xwlazy.lazy.manifest
5
- --------------------
4
+ xwlazy.common.utils.manifest
5
+ ----------------------------
6
6
 
7
7
  Centralized loader for per-package dependency manifests. A manifest can be
8
8
  declared either as a JSON file located in the target project's root directory
@@ -18,7 +18,7 @@ It also keeps lightweight caches with file-modification tracking so repeated
18
18
  lookups do not hit the filesystem unnecessarily.
19
19
  """
20
20
 
21
- from dataclasses import dataclass, field
21
+ from dataclasses import field
22
22
  import importlib.util
23
23
  import json
24
24
  import os
@@ -67,27 +67,8 @@ def _normalize_wrap_hints(values: Iterable[Any]) -> List[str]:
67
67
  return hints
68
68
 
69
69
 
70
- @dataclass(frozen=True)
71
- class PackageManifest:
72
- """Resolved manifest data for a single package."""
73
-
74
- package: str
75
- dependencies: Dict[str, str] = field(default_factory=dict)
76
- watched_prefixes: Tuple[str, ...] = ()
77
- async_installs: bool = False
78
- async_workers: int = 1
79
- class_wrap_prefixes: Tuple[str, ...] = ()
80
- metadata: Dict[str, Any] = field(default_factory=dict)
81
-
82
- def get_dependency(self, import_name: str) -> Optional[str]:
83
- """Return the declared package for the given import name."""
84
- if not import_name:
85
- return None
86
- direct = self.dependencies.get(import_name)
87
- if direct is not None:
88
- return direct
89
- # Case-insensitive fallback for convenience
90
- return self.dependencies.get(import_name.lower())
70
+ # PackageManifest moved to defs.py - import it from there
71
+ from ...defs import PackageManifest
91
72
 
92
73
 
93
74
  class LazyManifestLoader:
@@ -133,6 +114,28 @@ class LazyManifestLoader:
133
114
  self._shared_dependency_maps.clear()
134
115
  self._generation += 1
135
116
 
117
+ def sync_manifest_configuration(self, package_name: str) -> None:
118
+ """
119
+ Sync configuration from manifest for a specific package.
120
+
121
+ This method forces a reload of the manifest and clears caches
122
+ to ensure the latest configuration is used.
123
+
124
+ Args:
125
+ package_name: The package name to sync configuration for
126
+ """
127
+ with self._lock:
128
+ # Clear cache for this package
129
+ package_key = _normalize_package_name(package_name)
130
+ if package_key in self._manifest_cache:
131
+ del self._manifest_cache[package_key]
132
+ if package_key in self._source_signatures:
133
+ del self._source_signatures[package_key]
134
+ # Increment generation to invalidate shared caches
135
+ self._generation += 1
136
+ # Force reload by getting manifest
137
+ self.get_manifest(package_key)
138
+
136
139
  # --------------------------------------------------------------------- #
137
140
  # Public API
138
141
  # --------------------------------------------------------------------- #
@@ -479,11 +482,25 @@ def refresh_manifest_cache() -> None:
479
482
  loader.clear_cache()
480
483
 
481
484
 
485
+ def sync_manifest_configuration(package_name: str) -> None:
486
+ """
487
+ Sync configuration from manifest for a specific package.
488
+
489
+ This is a convenience function that calls the manifest loader's
490
+ sync_manifest_configuration method.
491
+
492
+ Args:
493
+ package_name: The package name to sync configuration for
494
+ """
495
+ loader = get_manifest_loader()
496
+ loader.sync_manifest_configuration(package_name)
497
+
498
+
482
499
  __all__ = [
483
500
  "PackageManifest",
484
501
  "LazyManifestLoader",
485
502
  "get_manifest_loader",
486
503
  "refresh_manifest_cache",
504
+ "sync_manifest_configuration",
487
505
  ]
488
506
 
489
-
@@ -0,0 +1,188 @@
1
+ """
2
+ Strategy 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 store custom strategies per package for both package and module operations.
11
+ """
12
+
13
+ import threading
14
+ from typing import Dict, Optional, Any, TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from ...contracts import (
18
+ IInstallExecutionStrategy,
19
+ IInstallTimingStrategy,
20
+ IDiscoveryStrategy,
21
+ IPolicyStrategy,
22
+ IMappingStrategy,
23
+ IModuleHelperStrategy,
24
+ IModuleManagerStrategy,
25
+ ICachingStrategy,
26
+ )
27
+
28
+
29
+ class StrategyRegistry:
30
+ """Registry to store custom strategies per package."""
31
+
32
+ # Package strategies
33
+ _package_execution_strategies: Dict[str, 'IInstallExecutionStrategy'] = {}
34
+ _package_timing_strategies: Dict[str, 'IInstallTimingStrategy'] = {}
35
+ _package_discovery_strategies: Dict[str, 'IDiscoveryStrategy'] = {}
36
+ _package_policy_strategies: Dict[str, 'IPolicyStrategy'] = {}
37
+ _package_mapping_strategies: Dict[str, 'IMappingStrategy'] = {}
38
+
39
+ # Module strategies
40
+ _module_helper_strategies: Dict[str, 'IModuleHelperStrategy'] = {}
41
+ _module_manager_strategies: Dict[str, 'IModuleManagerStrategy'] = {}
42
+ _module_caching_strategies: Dict[str, 'ICachingStrategy'] = {}
43
+
44
+ _lock = threading.RLock()
45
+
46
+ @classmethod
47
+ def set_package_strategy(
48
+ cls,
49
+ package_name: str,
50
+ strategy_type: str,
51
+ strategy: Any,
52
+ ) -> None:
53
+ """
54
+ Set a package strategy for a package.
55
+
56
+ Args:
57
+ package_name: Package name
58
+ strategy_type: One of 'execution', 'timing', 'discovery', 'policy', 'mapping'
59
+ strategy: Strategy instance
60
+ """
61
+ package_key = package_name.lower()
62
+ with cls._lock:
63
+ if strategy_type == 'execution':
64
+ cls._package_execution_strategies[package_key] = strategy
65
+ elif strategy_type == 'timing':
66
+ cls._package_timing_strategies[package_key] = strategy
67
+ elif strategy_type == 'discovery':
68
+ cls._package_discovery_strategies[package_key] = strategy
69
+ elif strategy_type == 'policy':
70
+ cls._package_policy_strategies[package_key] = strategy
71
+ elif strategy_type == 'mapping':
72
+ cls._package_mapping_strategies[package_key] = strategy
73
+ else:
74
+ raise ValueError(f"Unknown package strategy type: {strategy_type}")
75
+
76
+ @classmethod
77
+ def get_package_strategy(
78
+ cls,
79
+ package_name: str,
80
+ strategy_type: str,
81
+ ) -> Optional[Any]:
82
+ """
83
+ Get a package strategy for a package.
84
+
85
+ Args:
86
+ package_name: Package name
87
+ strategy_type: One of 'execution', 'timing', 'discovery', 'policy', 'mapping'
88
+
89
+ Returns:
90
+ Strategy instance or None if not set
91
+ """
92
+ package_key = package_name.lower()
93
+ with cls._lock:
94
+ if strategy_type == 'execution':
95
+ return cls._package_execution_strategies.get(package_key)
96
+ elif strategy_type == 'timing':
97
+ return cls._package_timing_strategies.get(package_key)
98
+ elif strategy_type == 'discovery':
99
+ return cls._package_discovery_strategies.get(package_key)
100
+ elif strategy_type == 'policy':
101
+ return cls._package_policy_strategies.get(package_key)
102
+ elif strategy_type == 'mapping':
103
+ return cls._package_mapping_strategies.get(package_key)
104
+ else:
105
+ raise ValueError(f"Unknown package strategy type: {strategy_type}")
106
+
107
+ @classmethod
108
+ def set_module_strategy(
109
+ cls,
110
+ package_name: str,
111
+ strategy_type: str,
112
+ strategy: Any,
113
+ ) -> None:
114
+ """
115
+ Set a module strategy for a package.
116
+
117
+ Args:
118
+ package_name: Package name
119
+ strategy_type: One of 'helper', 'manager', 'caching'
120
+ strategy: Strategy instance
121
+ """
122
+ package_key = package_name.lower()
123
+ with cls._lock:
124
+ if strategy_type == 'helper':
125
+ cls._module_helper_strategies[package_key] = strategy
126
+ elif strategy_type == 'manager':
127
+ cls._module_manager_strategies[package_key] = strategy
128
+ elif strategy_type == 'caching':
129
+ cls._module_caching_strategies[package_key] = strategy
130
+ else:
131
+ raise ValueError(f"Unknown module strategy type: {strategy_type}")
132
+
133
+ @classmethod
134
+ def get_module_strategy(
135
+ cls,
136
+ package_name: str,
137
+ strategy_type: str,
138
+ ) -> Optional[Any]:
139
+ """
140
+ Get a module strategy for a package.
141
+
142
+ Args:
143
+ package_name: Package name
144
+ strategy_type: One of 'helper', 'manager', 'caching'
145
+
146
+ Returns:
147
+ Strategy instance or None if not set
148
+ """
149
+ package_key = package_name.lower()
150
+ with cls._lock:
151
+ if strategy_type == 'helper':
152
+ return cls._module_helper_strategies.get(package_key)
153
+ elif strategy_type == 'manager':
154
+ return cls._module_manager_strategies.get(package_key)
155
+ elif strategy_type == 'caching':
156
+ return cls._module_caching_strategies.get(package_key)
157
+ else:
158
+ raise ValueError(f"Unknown module strategy type: {strategy_type}")
159
+
160
+ @classmethod
161
+ def clear_package_strategies(cls, package_name: str) -> None:
162
+ """Clear all package strategies for a package."""
163
+ package_key = package_name.lower()
164
+ with cls._lock:
165
+ cls._package_execution_strategies.pop(package_key, None)
166
+ cls._package_timing_strategies.pop(package_key, None)
167
+ cls._package_discovery_strategies.pop(package_key, None)
168
+ cls._package_policy_strategies.pop(package_key, None)
169
+ cls._package_mapping_strategies.pop(package_key, None)
170
+
171
+ @classmethod
172
+ def clear_module_strategies(cls, package_name: str) -> None:
173
+ """Clear all module strategies for a package."""
174
+ package_key = package_name.lower()
175
+ with cls._lock:
176
+ cls._module_helper_strategies.pop(package_key, None)
177
+ cls._module_manager_strategies.pop(package_key, None)
178
+ cls._module_caching_strategies.pop(package_key, None)
179
+
180
+ @classmethod
181
+ def clear_all_strategies(cls, package_name: str) -> None:
182
+ """Clear all strategies (package and module) for a package."""
183
+ cls.clear_package_strategies(package_name)
184
+ cls.clear_module_strategies(package_name)
185
+
186
+
187
+ __all__ = ['StrategyRegistry']
188
+
@@ -0,0 +1,57 @@
1
+ """
2
+ Package Strategies
3
+
4
+ All package strategy implementations.
5
+ """
6
+
7
+ # Mapping strategies
8
+ from .package_mapping_manifest_first import ManifestFirstMapping
9
+ from .package_mapping_discovery_first import DiscoveryFirstMapping
10
+ from .package_mapping_hybrid import HybridMapping
11
+
12
+ # Policy strategies
13
+ from .package_policy_permissive import PermissivePolicy
14
+ from .package_policy_allow_list import AllowListPolicy
15
+ from .package_policy_deny_list import DenyListPolicy
16
+
17
+ # Timing strategies
18
+ from .package_timing_smart import SmartTiming
19
+ from .package_timing_full import FullTiming
20
+ from .package_timing_clean import CleanTiming
21
+ from .package_timing_temporary import TemporaryTiming
22
+
23
+ # Execution strategies
24
+ from .package_execution_pip import PipExecution
25
+ from .package_execution_wheel import WheelExecution
26
+ from .package_execution_cached import CachedExecution
27
+ from .package_execution_async import AsyncExecution
28
+
29
+ # Discovery strategies
30
+ from .package_discovery_file import FileBasedDiscovery
31
+ from .package_discovery_manifest import ManifestBasedDiscovery
32
+ from .package_discovery_hybrid import HybridDiscovery
33
+
34
+ __all__ = [
35
+ # Mapping strategies
36
+ 'ManifestFirstMapping',
37
+ 'DiscoveryFirstMapping',
38
+ 'HybridMapping',
39
+ # Policy strategies
40
+ 'PermissivePolicy',
41
+ 'AllowListPolicy',
42
+ 'DenyListPolicy',
43
+ # Timing strategies
44
+ 'SmartTiming',
45
+ 'FullTiming',
46
+ 'CleanTiming',
47
+ 'TemporaryTiming',
48
+ # Execution strategies
49
+ 'PipExecution',
50
+ 'WheelExecution',
51
+ 'CachedExecution',
52
+ 'AsyncExecution',
53
+ # Discovery strategies
54
+ 'FileBasedDiscovery',
55
+ 'ManifestBasedDiscovery',
56
+ 'HybridDiscovery',
57
+ ]
@@ -0,0 +1,130 @@
1
+ """
2
+ File-Based Discovery Strategy
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
+ File-based discovery - discovers dependencies from project files.
11
+ """
12
+
13
+ from pathlib import Path
14
+ from typing import Dict, Optional, Any
15
+ from ...package.base import ADiscoveryStrategy
16
+
17
+
18
+ class FileBasedDiscovery(ADiscoveryStrategy):
19
+ """
20
+ File-based discovery strategy - discovers dependencies from project files.
21
+
22
+ Reads from pyproject.toml, requirements.txt, setup.py, etc.
23
+ """
24
+
25
+ def __init__(self, project_root: Optional[Path] = None):
26
+ """
27
+ Initialize file-based discovery strategy.
28
+
29
+ Args:
30
+ project_root: Project root directory (auto-detected if None)
31
+ """
32
+ self._project_root = project_root or self._detect_project_root()
33
+
34
+ def _detect_project_root(self) -> Path:
35
+ """Detect project root directory."""
36
+ import os
37
+ cwd = Path.cwd()
38
+
39
+ # Look for common project markers
40
+ markers = ['pyproject.toml', 'requirements.txt', 'setup.py', '.git']
41
+ for marker in markers:
42
+ current = cwd
43
+ for _ in range(5): # Search up to 5 levels
44
+ if (current / marker).exists():
45
+ return current
46
+ parent = current.parent
47
+ if parent == current: # Reached filesystem root
48
+ break
49
+ current = parent
50
+
51
+ return cwd
52
+
53
+ def discover(self, project_root: Any = None) -> Dict[str, str]:
54
+ """
55
+ Discover dependencies from project files.
56
+
57
+ Args:
58
+ project_root: Optional project root (uses instance root if None)
59
+
60
+ Returns:
61
+ Dict mapping import_name -> package_name
62
+ """
63
+ root = Path(project_root) if project_root else self._project_root
64
+ dependencies = {}
65
+
66
+ # Check pyproject.toml
67
+ pyproject = root / "pyproject.toml"
68
+ if pyproject.exists():
69
+ try:
70
+ import tomllib
71
+ except ImportError:
72
+ try:
73
+ import tomli as tomllib
74
+ except ImportError:
75
+ tomllib = None
76
+
77
+ if tomllib:
78
+ with open(pyproject, 'rb') as f:
79
+ data = tomllib.load(f)
80
+ project = data.get('project', {})
81
+ deps = project.get('dependencies', [])
82
+ for dep in deps:
83
+ # Parse dependency spec (e.g., "pandas>=1.0" -> "pandas")
84
+ package_name = dep.split('>=')[0].split('==')[0].split('!=')[0].strip()
85
+ # Use package name as import name (heuristic)
86
+ import_name = package_name.replace('-', '_')
87
+ dependencies[import_name] = package_name
88
+
89
+ # Check requirements.txt
90
+ requirements = root / "requirements.txt"
91
+ if requirements.exists():
92
+ with open(requirements, 'r', encoding='utf-8') as f:
93
+ for line in f:
94
+ line = line.strip()
95
+ if line and not line.startswith('#'):
96
+ # Parse requirement (e.g., "pandas>=1.0" -> "pandas")
97
+ package_name = line.split('>=')[0].split('==')[0].split('!=')[0].strip()
98
+ import_name = package_name.replace('-', '_')
99
+ dependencies[import_name] = package_name
100
+
101
+ return dependencies
102
+
103
+ def get_source(self, import_name: str) -> Optional[str]:
104
+ """
105
+ Get the source of a discovered dependency.
106
+
107
+ Args:
108
+ import_name: Import name to check
109
+
110
+ Returns:
111
+ Source file name or None
112
+ """
113
+ root = self._project_root
114
+
115
+ # Check pyproject.toml
116
+ pyproject = root / "pyproject.toml"
117
+ if pyproject.exists():
118
+ deps = self.discover()
119
+ if import_name in deps:
120
+ return "pyproject.toml"
121
+
122
+ # Check requirements.txt
123
+ requirements = root / "requirements.txt"
124
+ if requirements.exists():
125
+ deps = self.discover()
126
+ if import_name in deps:
127
+ return "requirements.txt"
128
+
129
+ return None
130
+
@@ -0,0 +1,85 @@
1
+ """
2
+ Hybrid Discovery Strategy
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
+ Hybrid discovery - combines file-based and manifest-based discovery.
11
+ """
12
+
13
+ from pathlib import Path
14
+ from typing import Dict, Optional, Any
15
+ from ...package.base import ADiscoveryStrategy
16
+ from .package_discovery_file import FileBasedDiscovery
17
+ from .package_discovery_manifest import ManifestBasedDiscovery
18
+
19
+
20
+ class HybridDiscovery(ADiscoveryStrategy):
21
+ """
22
+ Hybrid discovery strategy - combines file-based and manifest-based discovery.
23
+
24
+ Priority: Manifest > File-based
25
+ """
26
+
27
+ def __init__(self, package_name: str = 'default', project_root: Optional[Path] = None):
28
+ """
29
+ Initialize hybrid discovery strategy.
30
+
31
+ Args:
32
+ package_name: Package name for isolation
33
+ project_root: Project root directory (auto-detected if None)
34
+ """
35
+ self._package_name = package_name
36
+ self._project_root = project_root
37
+ self._file_discovery = FileBasedDiscovery(project_root)
38
+ self._manifest_discovery = ManifestBasedDiscovery(package_name, project_root)
39
+
40
+ def discover(self, project_root: Any = None) -> Dict[str, str]:
41
+ """
42
+ Discover dependencies from all sources.
43
+
44
+ Priority: Manifest > File-based
45
+
46
+ Args:
47
+ project_root: Optional project root (uses instance root if None)
48
+
49
+ Returns:
50
+ Dict mapping import_name -> package_name
51
+ """
52
+ dependencies = {}
53
+
54
+ # First, get file-based dependencies
55
+ file_deps = self._file_discovery.discover(project_root)
56
+ dependencies.update(file_deps)
57
+
58
+ # Then, overlay manifest dependencies (takes precedence)
59
+ manifest_deps = self._manifest_discovery.discover(project_root)
60
+ dependencies.update(manifest_deps) # Manifest overrides file-based
61
+
62
+ return dependencies
63
+
64
+ def get_source(self, import_name: str) -> Optional[str]:
65
+ """
66
+ Get the source of a discovered dependency.
67
+
68
+ Args:
69
+ import_name: Import name to check
70
+
71
+ Returns:
72
+ Source file name or "hybrid"
73
+ """
74
+ # Check manifest first (higher priority)
75
+ manifest_source = self._manifest_discovery.get_source(import_name)
76
+ if manifest_source:
77
+ return manifest_source
78
+
79
+ # Check file-based
80
+ file_source = self._file_discovery.get_source(import_name)
81
+ if file_source:
82
+ return file_source
83
+
84
+ return None
85
+
@@ -0,0 +1,102 @@
1
+ """
2
+ Manifest-Based Discovery Strategy
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
+ Manifest-based discovery - discovers dependencies from manifest files.
11
+ """
12
+
13
+ from pathlib import Path
14
+ from typing import Dict, Optional, Any
15
+ from ...package.base import ADiscoveryStrategy
16
+
17
+
18
+ class ManifestBasedDiscovery(ADiscoveryStrategy):
19
+ """
20
+ Manifest-based discovery strategy - discovers dependencies from manifest files.
21
+
22
+ Reads from xwlazy.manifest.json or pyproject.toml [tool.xwlazy] section.
23
+ """
24
+
25
+ def __init__(self, package_name: str = 'default', project_root: Optional[Path] = None):
26
+ """
27
+ Initialize manifest-based discovery strategy.
28
+
29
+ Args:
30
+ package_name: Package name for isolation
31
+ project_root: Project root directory (auto-detected if None)
32
+ """
33
+ self._package_name = package_name
34
+ self._project_root = project_root or self._detect_project_root()
35
+
36
+ def _detect_project_root(self) -> Path:
37
+ """Detect project root directory."""
38
+ import os
39
+ cwd = Path.cwd()
40
+
41
+ # Look for manifest files
42
+ markers = ['xwlazy.manifest.json', 'lazy.manifest.json', '.xwlazy.manifest.json', 'pyproject.toml']
43
+ for marker in markers:
44
+ current = cwd
45
+ for _ in range(5): # Search up to 5 levels
46
+ if (current / marker).exists():
47
+ return current
48
+ parent = current.parent
49
+ if parent == current: # Reached filesystem root
50
+ break
51
+ current = parent
52
+
53
+ return cwd
54
+
55
+ def discover(self, project_root: Any = None) -> Dict[str, str]:
56
+ """
57
+ Discover dependencies from manifest files.
58
+
59
+ Args:
60
+ project_root: Optional project root (uses instance root if None)
61
+
62
+ Returns:
63
+ Dict mapping import_name -> package_name
64
+ """
65
+ # Lazy import to avoid circular dependency
66
+ from ...package.manifest import get_manifest_loader
67
+
68
+ loader = get_manifest_loader()
69
+ manifest = loader.get_manifest(self._package_name)
70
+
71
+ if manifest and manifest.dependencies:
72
+ return manifest.dependencies.copy()
73
+
74
+ return {}
75
+
76
+ def get_source(self, import_name: str) -> Optional[str]:
77
+ """
78
+ Get the source of a discovered dependency.
79
+
80
+ Args:
81
+ import_name: Import name to check
82
+
83
+ Returns:
84
+ Source file name (e.g., "xwlazy.manifest.json")
85
+ """
86
+ from ...package.manifest import get_manifest_loader
87
+
88
+ loader = get_manifest_loader()
89
+ manifest = loader.get_manifest(self._package_name)
90
+
91
+ if manifest and manifest.dependencies and import_name in manifest.dependencies:
92
+ # Try to find which file was used
93
+ root = self._project_root
94
+ for filename in ['xwlazy.manifest.json', 'lazy.manifest.json', '.xwlazy.manifest.json']:
95
+ if (root / filename).exists():
96
+ return filename
97
+ # Check pyproject.toml
98
+ if (root / "pyproject.toml").exists():
99
+ return "pyproject.toml [tool.xwlazy]"
100
+
101
+ return None
102
+