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,496 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
xwlazy.common.utils.manifest
|
|
5
|
-
----------------------------
|
|
6
|
-
|
|
7
|
-
Centralized loader for per-package dependency manifests. A manifest can be
|
|
8
|
-
declared either as a JSON file located in the target project's root directory
|
|
9
|
-
or inline inside ``pyproject.toml`` under the ``[tool.xwlazy]`` namespace.
|
|
10
|
-
|
|
11
|
-
The loader consolidates the following pieces of information:
|
|
12
|
-
|
|
13
|
-
* Explicit import -> package mappings
|
|
14
|
-
* Serialization/watch prefixes that should be handled by the import hook
|
|
15
|
-
* Async installation preferences (queue enabled + worker count)
|
|
16
|
-
|
|
17
|
-
It also keeps lightweight caches with file-modification tracking so repeated
|
|
18
|
-
lookups do not hit the filesystem unnecessarily.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
from dataclasses import field
|
|
22
|
-
import importlib.util
|
|
23
|
-
import json
|
|
24
|
-
import os
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
from threading import RLock
|
|
27
|
-
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
|
28
|
-
|
|
29
|
-
try: # Python 3.11+
|
|
30
|
-
import tomllib # type: ignore[attr-defined]
|
|
31
|
-
except Exception: # pragma: no cover - fallback for <=3.10
|
|
32
|
-
try:
|
|
33
|
-
import tomli as tomllib # type: ignore[attr-defined]
|
|
34
|
-
except ImportError: # pragma: no cover
|
|
35
|
-
tomllib = None # type: ignore
|
|
36
|
-
|
|
37
|
-
DEFAULT_MANIFEST_FILENAMES: Tuple[str, ...] = (
|
|
38
|
-
"xwlazy.manifest.json",
|
|
39
|
-
"lazy.manifest.json",
|
|
40
|
-
".xwlazy.manifest.json",
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
ENV_MANIFEST_PATH = "XWLAZY_MANIFEST_PATH"
|
|
44
|
-
|
|
45
|
-
def _normalize_package_name(package_name: Optional[str]) -> str:
|
|
46
|
-
return (package_name or "global").strip().lower()
|
|
47
|
-
|
|
48
|
-
def _normalize_prefix(prefix: str) -> str:
|
|
49
|
-
prefix = prefix.strip()
|
|
50
|
-
if not prefix:
|
|
51
|
-
return ""
|
|
52
|
-
if not prefix.endswith("."):
|
|
53
|
-
prefix = f"{prefix}."
|
|
54
|
-
return prefix
|
|
55
|
-
|
|
56
|
-
def _normalize_wrap_hints(values: Iterable[Any]) -> List[str]:
|
|
57
|
-
hints: List[str] = []
|
|
58
|
-
for value in values:
|
|
59
|
-
if value is None:
|
|
60
|
-
continue
|
|
61
|
-
hint = str(value).strip().lower()
|
|
62
|
-
if hint:
|
|
63
|
-
hints.append(hint)
|
|
64
|
-
return hints
|
|
65
|
-
|
|
66
|
-
# PackageManifest moved to defs.py - import it from there
|
|
67
|
-
from ...defs import PackageManifest
|
|
68
|
-
|
|
69
|
-
class LazyManifestLoader:
|
|
70
|
-
"""
|
|
71
|
-
Loads and caches manifest data per package.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
default_root: Optional fallback root directory used when a package
|
|
75
|
-
root cannot be auto-detected.
|
|
76
|
-
package_roots: Optional explicit mapping used mainly for tests.
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
def __init__(
|
|
80
|
-
self,
|
|
81
|
-
default_root: Optional[Path] = None,
|
|
82
|
-
package_roots: Optional[Dict[str, Path]] = None,
|
|
83
|
-
) -> None:
|
|
84
|
-
self._default_root = default_root
|
|
85
|
-
self._provided_roots = {
|
|
86
|
-
_normalize_package_name(name): Path(path)
|
|
87
|
-
for name, path in (package_roots or {}).items()
|
|
88
|
-
}
|
|
89
|
-
self._manifest_cache: Dict[str, PackageManifest] = {}
|
|
90
|
-
self._source_signatures: Dict[str, Tuple[str, float, float]] = {}
|
|
91
|
-
self._pyproject_cache: Dict[Path, Tuple[float, Dict[str, Any]]] = {}
|
|
92
|
-
self._shared_dependency_maps: Dict[
|
|
93
|
-
Tuple[str, float, float], Dict[str, Dict[str, str]]
|
|
94
|
-
] = {}
|
|
95
|
-
self._lock = RLock()
|
|
96
|
-
self._generation = 0
|
|
97
|
-
|
|
98
|
-
@property
|
|
99
|
-
def generation(self) -> int:
|
|
100
|
-
"""Incremented whenever any manifest content changes."""
|
|
101
|
-
return self._generation
|
|
102
|
-
|
|
103
|
-
def clear_cache(self) -> None:
|
|
104
|
-
"""Forcefully clear cached manifests."""
|
|
105
|
-
with self._lock:
|
|
106
|
-
self._manifest_cache.clear()
|
|
107
|
-
self._source_signatures.clear()
|
|
108
|
-
self._pyproject_cache.clear()
|
|
109
|
-
self._shared_dependency_maps.clear()
|
|
110
|
-
self._generation += 1
|
|
111
|
-
|
|
112
|
-
def sync_manifest_configuration(self, package_name: str) -> None:
|
|
113
|
-
"""
|
|
114
|
-
Sync configuration from manifest for a specific package.
|
|
115
|
-
|
|
116
|
-
This method forces a reload of the manifest and clears caches
|
|
117
|
-
to ensure the latest configuration is used.
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
package_name: The package name to sync configuration for
|
|
121
|
-
"""
|
|
122
|
-
with self._lock:
|
|
123
|
-
# Clear cache for this package
|
|
124
|
-
package_key = _normalize_package_name(package_name)
|
|
125
|
-
if package_key in self._manifest_cache:
|
|
126
|
-
del self._manifest_cache[package_key]
|
|
127
|
-
if package_key in self._source_signatures:
|
|
128
|
-
del self._source_signatures[package_key]
|
|
129
|
-
# Increment generation to invalidate shared caches
|
|
130
|
-
self._generation += 1
|
|
131
|
-
# Force reload by getting manifest
|
|
132
|
-
self.get_manifest(package_key)
|
|
133
|
-
|
|
134
|
-
# --------------------------------------------------------------------- #
|
|
135
|
-
# Public API
|
|
136
|
-
# --------------------------------------------------------------------- #
|
|
137
|
-
def get_manifest(self, package_name: Optional[str]) -> Optional[PackageManifest]:
|
|
138
|
-
"""Return manifest for the provided package."""
|
|
139
|
-
key = _normalize_package_name(package_name)
|
|
140
|
-
with self._lock:
|
|
141
|
-
signature = self._compute_signature(key)
|
|
142
|
-
cached_signature = self._source_signatures.get(key)
|
|
143
|
-
if (
|
|
144
|
-
cached_signature is not None
|
|
145
|
-
and signature is not None
|
|
146
|
-
and cached_signature == signature
|
|
147
|
-
and key in self._manifest_cache
|
|
148
|
-
):
|
|
149
|
-
return self._manifest_cache[key]
|
|
150
|
-
|
|
151
|
-
manifest = self._load_manifest(key)
|
|
152
|
-
if manifest is None:
|
|
153
|
-
# Cache miss is still tracked so we don't re-read files
|
|
154
|
-
self._manifest_cache.pop(key, None)
|
|
155
|
-
self._source_signatures[key] = signature or ("", 0.0, 0.0)
|
|
156
|
-
return None
|
|
157
|
-
|
|
158
|
-
self._manifest_cache[key] = manifest
|
|
159
|
-
if signature is not None:
|
|
160
|
-
self._source_signatures[key] = signature
|
|
161
|
-
per_signature = self._shared_dependency_maps.setdefault(signature, {})
|
|
162
|
-
per_signature[manifest.package] = manifest.dependencies.copy()
|
|
163
|
-
self._generation += 1
|
|
164
|
-
return manifest
|
|
165
|
-
|
|
166
|
-
def get_manifest_signature(self, package_name: Optional[str]) -> Optional[Tuple[str, float, float]]:
|
|
167
|
-
key = _normalize_package_name(package_name)
|
|
168
|
-
with self._lock:
|
|
169
|
-
signature = self._source_signatures.get(key)
|
|
170
|
-
if signature is not None:
|
|
171
|
-
return signature
|
|
172
|
-
signature = self._compute_signature(key)
|
|
173
|
-
if signature is not None:
|
|
174
|
-
self._source_signatures[key] = signature
|
|
175
|
-
return signature
|
|
176
|
-
|
|
177
|
-
def get_shared_dependencies(
|
|
178
|
-
self,
|
|
179
|
-
package_name: Optional[str],
|
|
180
|
-
signature: Optional[Tuple[str, float, float]],
|
|
181
|
-
) -> Optional[Dict[str, str]]:
|
|
182
|
-
if signature is None:
|
|
183
|
-
return None
|
|
184
|
-
with self._lock:
|
|
185
|
-
package_maps = self._shared_dependency_maps.get(signature)
|
|
186
|
-
if not package_maps:
|
|
187
|
-
return None
|
|
188
|
-
key = _normalize_package_name(package_name)
|
|
189
|
-
return package_maps.get(key)
|
|
190
|
-
|
|
191
|
-
# ------------------------------------------------------------------ #
|
|
192
|
-
# Internal helpers
|
|
193
|
-
# ------------------------------------------------------------------ #
|
|
194
|
-
def _load_manifest(self, package_key: str) -> Optional[PackageManifest]:
|
|
195
|
-
root = self._resolve_project_root(package_key)
|
|
196
|
-
pyproject_path = root / "pyproject.toml"
|
|
197
|
-
pyproject_data = self._load_pyproject(pyproject_path)
|
|
198
|
-
json_data, manifest_path = self._load_json_manifest(root, pyproject_data)
|
|
199
|
-
|
|
200
|
-
data = self._merge_sources(package_key, pyproject_data, json_data)
|
|
201
|
-
if not data["dependencies"] and not data["watched_prefixes"] and not data["async_installs"]:
|
|
202
|
-
return None
|
|
203
|
-
|
|
204
|
-
wrap_prefixes = tuple(data.get("wrap_class_prefixes", ()))
|
|
205
|
-
|
|
206
|
-
manifest = PackageManifest(
|
|
207
|
-
package=package_key,
|
|
208
|
-
dependencies=data["dependencies"],
|
|
209
|
-
watched_prefixes=tuple(
|
|
210
|
-
_normalize_prefix(prefix)
|
|
211
|
-
for prefix in data["watched_prefixes"]
|
|
212
|
-
if _normalize_prefix(prefix)
|
|
213
|
-
),
|
|
214
|
-
async_installs=bool(data.get("async_installs")),
|
|
215
|
-
async_workers=max(1, int(data.get("async_workers", 1))),
|
|
216
|
-
class_wrap_prefixes=wrap_prefixes,
|
|
217
|
-
metadata={
|
|
218
|
-
"root": str(root),
|
|
219
|
-
"manifest_path": str(manifest_path) if manifest_path else None,
|
|
220
|
-
"wrap_class_prefixes": wrap_prefixes,
|
|
221
|
-
},
|
|
222
|
-
)
|
|
223
|
-
return manifest
|
|
224
|
-
|
|
225
|
-
def _compute_signature(self, package_key: str) -> Optional[Tuple[str, float, float]]:
|
|
226
|
-
root = self._resolve_project_root(package_key)
|
|
227
|
-
pyproject_path = root / "pyproject.toml"
|
|
228
|
-
pyproject_mtime = pyproject_path.stat().st_mtime if pyproject_path.exists() else 0.0
|
|
229
|
-
manifest_path = self._resolve_manifest_path(root, pyproject_path)
|
|
230
|
-
json_mtime = manifest_path.stat().st_mtime if manifest_path and manifest_path.exists() else 0.0
|
|
231
|
-
env_token = os.environ.get(ENV_MANIFEST_PATH, "")
|
|
232
|
-
if not manifest_path and not pyproject_path.exists() and not env_token:
|
|
233
|
-
return None
|
|
234
|
-
return (env_token + str(manifest_path), pyproject_mtime, json_mtime)
|
|
235
|
-
|
|
236
|
-
def _resolve_project_root(self, package_key: str) -> Path:
|
|
237
|
-
if package_key in self._provided_roots:
|
|
238
|
-
return self._provided_roots[package_key]
|
|
239
|
-
|
|
240
|
-
module_candidates: Iterable[str]
|
|
241
|
-
if package_key == "global":
|
|
242
|
-
module_candidates = ()
|
|
243
|
-
else:
|
|
244
|
-
module_candidates = (f"exonware.{package_key}", package_key)
|
|
245
|
-
|
|
246
|
-
for module_name in module_candidates:
|
|
247
|
-
spec = importlib.util.find_spec(module_name)
|
|
248
|
-
if spec and spec.origin:
|
|
249
|
-
origin_path = Path(spec.origin).resolve()
|
|
250
|
-
root = self._walk_to_project_root(origin_path.parent)
|
|
251
|
-
if root:
|
|
252
|
-
self._provided_roots[package_key] = root
|
|
253
|
-
return root
|
|
254
|
-
|
|
255
|
-
if self._default_root:
|
|
256
|
-
return self._default_root
|
|
257
|
-
return Path.cwd()
|
|
258
|
-
|
|
259
|
-
@staticmethod
|
|
260
|
-
def _walk_to_project_root(start: Path) -> Optional[Path]:
|
|
261
|
-
current = start
|
|
262
|
-
while True:
|
|
263
|
-
if (current / "pyproject.toml").exists():
|
|
264
|
-
return current
|
|
265
|
-
parent = current.parent
|
|
266
|
-
if parent == current:
|
|
267
|
-
break
|
|
268
|
-
current = parent
|
|
269
|
-
return None
|
|
270
|
-
|
|
271
|
-
# ------------------------------- #
|
|
272
|
-
# Pyproject helpers
|
|
273
|
-
# ------------------------------- #
|
|
274
|
-
def _load_pyproject(self, path: Path) -> Dict[str, Any]:
|
|
275
|
-
if not path.exists() or tomllib is None:
|
|
276
|
-
return {}
|
|
277
|
-
|
|
278
|
-
cached = self._pyproject_cache.get(path)
|
|
279
|
-
current_mtime = path.stat().st_mtime
|
|
280
|
-
if cached and cached[0] == current_mtime:
|
|
281
|
-
return cached[1]
|
|
282
|
-
|
|
283
|
-
try:
|
|
284
|
-
with path.open("rb") as handle:
|
|
285
|
-
data = tomllib.load(handle)
|
|
286
|
-
except Exception:
|
|
287
|
-
data = {}
|
|
288
|
-
|
|
289
|
-
self._pyproject_cache[path] = (current_mtime, data)
|
|
290
|
-
return data
|
|
291
|
-
|
|
292
|
-
def _extract_pyproject_entry(
|
|
293
|
-
self,
|
|
294
|
-
pyproject_data: Dict[str, Any],
|
|
295
|
-
package_key: str,
|
|
296
|
-
) -> Dict[str, Any]:
|
|
297
|
-
tool_section = pyproject_data.get("tool", {})
|
|
298
|
-
lazy_section = tool_section.get("xwlazy", {})
|
|
299
|
-
packages = lazy_section.get("packages", {})
|
|
300
|
-
entry = packages.get(package_key, {}) or packages.get(package_key.upper(), {})
|
|
301
|
-
|
|
302
|
-
dependencies = {}
|
|
303
|
-
watched = []
|
|
304
|
-
async_installs = lazy_section.get("async_installs") or entry.get("async_installs")
|
|
305
|
-
async_workers = entry.get("async_workers") or lazy_section.get("async_workers")
|
|
306
|
-
wrap_hints = []
|
|
307
|
-
|
|
308
|
-
global_deps = lazy_section.get("dependencies", {})
|
|
309
|
-
if isinstance(global_deps, dict):
|
|
310
|
-
dependencies.update({str(k): str(v) for k, v in global_deps.items()})
|
|
311
|
-
|
|
312
|
-
if "dependencies" in entry and isinstance(entry["dependencies"], dict):
|
|
313
|
-
dependencies.update({str(k): str(v) for k, v in entry["dependencies"].items()})
|
|
314
|
-
|
|
315
|
-
for key in ("watched-prefixes", "watched_prefixes", "watch"):
|
|
316
|
-
values = entry.get(key) or lazy_section.get(key)
|
|
317
|
-
if isinstance(values, list):
|
|
318
|
-
watched.extend(str(v) for v in values)
|
|
319
|
-
|
|
320
|
-
global_wrap = lazy_section.get("wrap_class_prefixes") or lazy_section.get("wrap_classes")
|
|
321
|
-
if isinstance(global_wrap, list):
|
|
322
|
-
wrap_hints.extend(_normalize_wrap_hints(global_wrap))
|
|
323
|
-
entry_wrap = entry.get("wrap_class_prefixes") or entry.get("wrap_classes")
|
|
324
|
-
if isinstance(entry_wrap, list):
|
|
325
|
-
wrap_hints.extend(_normalize_wrap_hints(entry_wrap))
|
|
326
|
-
|
|
327
|
-
return {
|
|
328
|
-
"dependencies": dependencies,
|
|
329
|
-
"watched_prefixes": watched,
|
|
330
|
-
"async_installs": bool(async_installs),
|
|
331
|
-
"async_workers": async_workers or 1,
|
|
332
|
-
"wrap_class_prefixes": wrap_hints,
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
# ------------------------------- #
|
|
336
|
-
# Manifest helpers
|
|
337
|
-
# ------------------------------- #
|
|
338
|
-
def _resolve_manifest_path(self, root: Path, pyproject_path: Path) -> Optional[Path]:
|
|
339
|
-
env_value = os.environ.get(ENV_MANIFEST_PATH)
|
|
340
|
-
if env_value:
|
|
341
|
-
for raw in env_value.split(os.pathsep):
|
|
342
|
-
candidate = Path(raw).expanduser()
|
|
343
|
-
if candidate.exists():
|
|
344
|
-
return candidate
|
|
345
|
-
|
|
346
|
-
if pyproject_path.exists() and tomllib is not None:
|
|
347
|
-
py_data = self._load_pyproject(pyproject_path)
|
|
348
|
-
tool_section = py_data.get("tool", {}).get("xwlazy", {})
|
|
349
|
-
manifest_path = tool_section.get("manifest") or tool_section.get("manifest_path")
|
|
350
|
-
if manifest_path:
|
|
351
|
-
candidate = (root / manifest_path).resolve()
|
|
352
|
-
if candidate.exists():
|
|
353
|
-
return candidate
|
|
354
|
-
|
|
355
|
-
for filename in DEFAULT_MANIFEST_FILENAMES:
|
|
356
|
-
candidate = root / filename
|
|
357
|
-
if candidate.exists():
|
|
358
|
-
return candidate
|
|
359
|
-
|
|
360
|
-
return None
|
|
361
|
-
|
|
362
|
-
def _load_json_manifest(
|
|
363
|
-
self,
|
|
364
|
-
root: Path,
|
|
365
|
-
pyproject_data: Dict[str, Any],
|
|
366
|
-
) -> Tuple[Dict[str, Any], Optional[Path]]:
|
|
367
|
-
manifest_path = self._resolve_manifest_path(root, root / "pyproject.toml")
|
|
368
|
-
if not manifest_path:
|
|
369
|
-
return {}, None
|
|
370
|
-
|
|
371
|
-
try:
|
|
372
|
-
with manifest_path.open("r", encoding="utf-8") as handle:
|
|
373
|
-
data = json.load(handle)
|
|
374
|
-
if isinstance(data, dict):
|
|
375
|
-
return data, manifest_path
|
|
376
|
-
except Exception:
|
|
377
|
-
pass
|
|
378
|
-
|
|
379
|
-
return {}, manifest_path
|
|
380
|
-
|
|
381
|
-
def _merge_sources(
|
|
382
|
-
self,
|
|
383
|
-
package_key: str,
|
|
384
|
-
pyproject_data: Dict[str, Any],
|
|
385
|
-
json_data: Dict[str, Any],
|
|
386
|
-
) -> Dict[str, Any]:
|
|
387
|
-
merged_dependencies: Dict[str, str] = {}
|
|
388
|
-
merged_watched: List[str] = []
|
|
389
|
-
merged_wrap_hints: List[str] = []
|
|
390
|
-
|
|
391
|
-
# Pyproject first (acts as baseline)
|
|
392
|
-
py_entry = self._extract_pyproject_entry(pyproject_data, package_key)
|
|
393
|
-
merged_dependencies.update({k.lower(): v for k, v in py_entry["dependencies"].items()})
|
|
394
|
-
merged_watched.extend(py_entry["watched_prefixes"])
|
|
395
|
-
merged_wrap_hints.extend(_normalize_wrap_hints(py_entry.get("wrap_class_prefixes", [])))
|
|
396
|
-
|
|
397
|
-
async_installs = py_entry.get("async_installs", False)
|
|
398
|
-
async_workers = py_entry.get("async_workers", 1)
|
|
399
|
-
|
|
400
|
-
# JSON global settings
|
|
401
|
-
global_deps = json_data.get("dependencies", {})
|
|
402
|
-
if isinstance(global_deps, dict):
|
|
403
|
-
merged_dependencies.update({str(k).lower(): str(v) for k, v in global_deps.items()})
|
|
404
|
-
|
|
405
|
-
global_watch = json_data.get("watch") or json_data.get("watched_prefixes")
|
|
406
|
-
if isinstance(global_watch, list):
|
|
407
|
-
merged_watched.extend(str(item) for item in global_watch)
|
|
408
|
-
global_wrap = json_data.get("wrap_class_prefixes") or json_data.get("wrap_classes")
|
|
409
|
-
if isinstance(global_wrap, list):
|
|
410
|
-
merged_wrap_hints.extend(_normalize_wrap_hints(global_wrap))
|
|
411
|
-
|
|
412
|
-
global_async = json_data.get("async_installs")
|
|
413
|
-
if global_async is not None:
|
|
414
|
-
async_installs = bool(global_async)
|
|
415
|
-
global_workers = json_data.get("async_workers")
|
|
416
|
-
if global_workers is not None:
|
|
417
|
-
async_workers = global_workers
|
|
418
|
-
|
|
419
|
-
packages_section = json_data.get("packages", {})
|
|
420
|
-
if isinstance(packages_section, dict):
|
|
421
|
-
entry = packages_section.get(package_key) or packages_section.get(package_key.upper())
|
|
422
|
-
if isinstance(entry, dict):
|
|
423
|
-
entry_deps = entry.get("dependencies", {})
|
|
424
|
-
if isinstance(entry_deps, dict):
|
|
425
|
-
merged_dependencies.update({str(k).lower(): str(v) for k, v in entry_deps.items()})
|
|
426
|
-
|
|
427
|
-
entry_watch = entry.get("watched_prefixes") or entry.get("watch")
|
|
428
|
-
if isinstance(entry_watch, list):
|
|
429
|
-
merged_watched.extend(str(item) for item in entry_watch)
|
|
430
|
-
entry_wrap = entry.get("wrap_class_prefixes") or entry.get("wrap_classes")
|
|
431
|
-
if isinstance(entry_wrap, list):
|
|
432
|
-
merged_wrap_hints.extend(_normalize_wrap_hints(entry_wrap))
|
|
433
|
-
|
|
434
|
-
if "async_installs" in entry:
|
|
435
|
-
async_installs = bool(entry["async_installs"])
|
|
436
|
-
if "async_workers" in entry:
|
|
437
|
-
async_workers = entry.get("async_workers", async_workers)
|
|
438
|
-
|
|
439
|
-
seen_wrap: Set[str] = set()
|
|
440
|
-
ordered_wrap_hints: List[str] = []
|
|
441
|
-
for hint in merged_wrap_hints:
|
|
442
|
-
if hint not in seen_wrap:
|
|
443
|
-
seen_wrap.add(hint)
|
|
444
|
-
ordered_wrap_hints.append(hint)
|
|
445
|
-
|
|
446
|
-
return {
|
|
447
|
-
"dependencies": merged_dependencies,
|
|
448
|
-
"watched_prefixes": merged_watched,
|
|
449
|
-
"async_installs": async_installs,
|
|
450
|
-
"async_workers": async_workers,
|
|
451
|
-
"wrap_class_prefixes": ordered_wrap_hints,
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
_manifest_loader: Optional[LazyManifestLoader] = None
|
|
455
|
-
_manifest_loader_lock = RLock()
|
|
456
|
-
|
|
457
|
-
def get_manifest_loader() -> LazyManifestLoader:
|
|
458
|
-
"""
|
|
459
|
-
Return the process-wide manifest loader instance.
|
|
460
|
-
|
|
461
|
-
Calling this function does not force any manifest to be loaded, but the
|
|
462
|
-
loader keeps shared caches that allow multiple subsystems (dependency mapper,
|
|
463
|
-
installer, hook configuration) to observe manifest changes consistently.
|
|
464
|
-
"""
|
|
465
|
-
global _manifest_loader
|
|
466
|
-
with _manifest_loader_lock:
|
|
467
|
-
if _manifest_loader is None:
|
|
468
|
-
_manifest_loader = LazyManifestLoader()
|
|
469
|
-
return _manifest_loader
|
|
470
|
-
|
|
471
|
-
def refresh_manifest_cache() -> None:
|
|
472
|
-
"""Forcefully clear the shared manifest loader cache."""
|
|
473
|
-
loader = get_manifest_loader()
|
|
474
|
-
loader.clear_cache()
|
|
475
|
-
|
|
476
|
-
def sync_manifest_configuration(package_name: str) -> None:
|
|
477
|
-
"""
|
|
478
|
-
Sync configuration from manifest for a specific package.
|
|
479
|
-
|
|
480
|
-
This is a convenience function that calls the manifest loader's
|
|
481
|
-
sync_manifest_configuration method.
|
|
482
|
-
|
|
483
|
-
Args:
|
|
484
|
-
package_name: The package name to sync configuration for
|
|
485
|
-
"""
|
|
486
|
-
loader = get_manifest_loader()
|
|
487
|
-
loader.sync_manifest_configuration(package_name)
|
|
488
|
-
|
|
489
|
-
__all__ = [
|
|
490
|
-
"PackageManifest",
|
|
491
|
-
"LazyManifestLoader",
|
|
492
|
-
"get_manifest_loader",
|
|
493
|
-
"refresh_manifest_cache",
|
|
494
|
-
"sync_manifest_configuration",
|
|
495
|
-
]
|
|
496
|
-
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Strategy Registry
|
|
3
|
-
|
|
4
|
-
Company: eXonware.com
|
|
5
|
-
Author: Eng. Muhammad AlShehri
|
|
6
|
-
Email: connect@exonware.com
|
|
7
|
-
|
|
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
|
-
class StrategyRegistry:
|
|
29
|
-
"""Registry to store custom strategies per package."""
|
|
30
|
-
|
|
31
|
-
# Package strategies
|
|
32
|
-
_package_execution_strategies: Dict[str, 'IInstallExecutionStrategy'] = {}
|
|
33
|
-
_package_timing_strategies: Dict[str, 'IInstallTimingStrategy'] = {}
|
|
34
|
-
_package_discovery_strategies: Dict[str, 'IDiscoveryStrategy'] = {}
|
|
35
|
-
_package_policy_strategies: Dict[str, 'IPolicyStrategy'] = {}
|
|
36
|
-
_package_mapping_strategies: Dict[str, 'IMappingStrategy'] = {}
|
|
37
|
-
|
|
38
|
-
# Module strategies
|
|
39
|
-
_module_helper_strategies: Dict[str, 'IModuleHelperStrategy'] = {}
|
|
40
|
-
_module_manager_strategies: Dict[str, 'IModuleManagerStrategy'] = {}
|
|
41
|
-
_module_caching_strategies: Dict[str, 'ICachingStrategy'] = {}
|
|
42
|
-
|
|
43
|
-
_lock = threading.RLock()
|
|
44
|
-
|
|
45
|
-
@classmethod
|
|
46
|
-
def set_package_strategy(
|
|
47
|
-
cls,
|
|
48
|
-
package_name: str,
|
|
49
|
-
strategy_type: str,
|
|
50
|
-
strategy: Any,
|
|
51
|
-
) -> None:
|
|
52
|
-
"""
|
|
53
|
-
Set a package strategy for a package.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
package_name: Package name
|
|
57
|
-
strategy_type: One of 'execution', 'timing', 'discovery', 'policy', 'mapping'
|
|
58
|
-
strategy: Strategy instance
|
|
59
|
-
"""
|
|
60
|
-
package_key = package_name.lower()
|
|
61
|
-
with cls._lock:
|
|
62
|
-
if strategy_type == 'execution':
|
|
63
|
-
cls._package_execution_strategies[package_key] = strategy
|
|
64
|
-
elif strategy_type == 'timing':
|
|
65
|
-
cls._package_timing_strategies[package_key] = strategy
|
|
66
|
-
elif strategy_type == 'discovery':
|
|
67
|
-
cls._package_discovery_strategies[package_key] = strategy
|
|
68
|
-
elif strategy_type == 'policy':
|
|
69
|
-
cls._package_policy_strategies[package_key] = strategy
|
|
70
|
-
elif strategy_type == 'mapping':
|
|
71
|
-
cls._package_mapping_strategies[package_key] = strategy
|
|
72
|
-
else:
|
|
73
|
-
raise ValueError(f"Unknown package strategy type: {strategy_type}")
|
|
74
|
-
|
|
75
|
-
@classmethod
|
|
76
|
-
def get_package_strategy(
|
|
77
|
-
cls,
|
|
78
|
-
package_name: str,
|
|
79
|
-
strategy_type: str,
|
|
80
|
-
) -> Optional[Any]:
|
|
81
|
-
"""
|
|
82
|
-
Get a package strategy for a package.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
package_name: Package name
|
|
86
|
-
strategy_type: One of 'execution', 'timing', 'discovery', 'policy', 'mapping'
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
Strategy instance or None if not set
|
|
90
|
-
"""
|
|
91
|
-
package_key = package_name.lower()
|
|
92
|
-
with cls._lock:
|
|
93
|
-
if strategy_type == 'execution':
|
|
94
|
-
return cls._package_execution_strategies.get(package_key)
|
|
95
|
-
elif strategy_type == 'timing':
|
|
96
|
-
return cls._package_timing_strategies.get(package_key)
|
|
97
|
-
elif strategy_type == 'discovery':
|
|
98
|
-
return cls._package_discovery_strategies.get(package_key)
|
|
99
|
-
elif strategy_type == 'policy':
|
|
100
|
-
return cls._package_policy_strategies.get(package_key)
|
|
101
|
-
elif strategy_type == 'mapping':
|
|
102
|
-
return cls._package_mapping_strategies.get(package_key)
|
|
103
|
-
else:
|
|
104
|
-
raise ValueError(f"Unknown package strategy type: {strategy_type}")
|
|
105
|
-
|
|
106
|
-
@classmethod
|
|
107
|
-
def set_module_strategy(
|
|
108
|
-
cls,
|
|
109
|
-
package_name: str,
|
|
110
|
-
strategy_type: str,
|
|
111
|
-
strategy: Any,
|
|
112
|
-
) -> None:
|
|
113
|
-
"""
|
|
114
|
-
Set a module strategy for a package.
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
package_name: Package name
|
|
118
|
-
strategy_type: One of 'helper', 'manager', 'caching'
|
|
119
|
-
strategy: Strategy instance
|
|
120
|
-
"""
|
|
121
|
-
package_key = package_name.lower()
|
|
122
|
-
with cls._lock:
|
|
123
|
-
if strategy_type == 'helper':
|
|
124
|
-
cls._module_helper_strategies[package_key] = strategy
|
|
125
|
-
elif strategy_type == 'manager':
|
|
126
|
-
cls._module_manager_strategies[package_key] = strategy
|
|
127
|
-
elif strategy_type == 'caching':
|
|
128
|
-
cls._module_caching_strategies[package_key] = strategy
|
|
129
|
-
else:
|
|
130
|
-
raise ValueError(f"Unknown module strategy type: {strategy_type}")
|
|
131
|
-
|
|
132
|
-
@classmethod
|
|
133
|
-
def get_module_strategy(
|
|
134
|
-
cls,
|
|
135
|
-
package_name: str,
|
|
136
|
-
strategy_type: str,
|
|
137
|
-
) -> Optional[Any]:
|
|
138
|
-
"""
|
|
139
|
-
Get a module strategy for a package.
|
|
140
|
-
|
|
141
|
-
Args:
|
|
142
|
-
package_name: Package name
|
|
143
|
-
strategy_type: One of 'helper', 'manager', 'caching'
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
Strategy instance or None if not set
|
|
147
|
-
"""
|
|
148
|
-
package_key = package_name.lower()
|
|
149
|
-
with cls._lock:
|
|
150
|
-
if strategy_type == 'helper':
|
|
151
|
-
return cls._module_helper_strategies.get(package_key)
|
|
152
|
-
elif strategy_type == 'manager':
|
|
153
|
-
return cls._module_manager_strategies.get(package_key)
|
|
154
|
-
elif strategy_type == 'caching':
|
|
155
|
-
return cls._module_caching_strategies.get(package_key)
|
|
156
|
-
else:
|
|
157
|
-
raise ValueError(f"Unknown module strategy type: {strategy_type}")
|
|
158
|
-
|
|
159
|
-
@classmethod
|
|
160
|
-
def clear_package_strategies(cls, package_name: str) -> None:
|
|
161
|
-
"""Clear all package strategies for a package."""
|
|
162
|
-
package_key = package_name.lower()
|
|
163
|
-
with cls._lock:
|
|
164
|
-
cls._package_execution_strategies.pop(package_key, None)
|
|
165
|
-
cls._package_timing_strategies.pop(package_key, None)
|
|
166
|
-
cls._package_discovery_strategies.pop(package_key, None)
|
|
167
|
-
cls._package_policy_strategies.pop(package_key, None)
|
|
168
|
-
cls._package_mapping_strategies.pop(package_key, None)
|
|
169
|
-
|
|
170
|
-
@classmethod
|
|
171
|
-
def clear_module_strategies(cls, package_name: str) -> None:
|
|
172
|
-
"""Clear all module strategies for a package."""
|
|
173
|
-
package_key = package_name.lower()
|
|
174
|
-
with cls._lock:
|
|
175
|
-
cls._module_helper_strategies.pop(package_key, None)
|
|
176
|
-
cls._module_manager_strategies.pop(package_key, None)
|
|
177
|
-
cls._module_caching_strategies.pop(package_key, None)
|
|
178
|
-
|
|
179
|
-
@classmethod
|
|
180
|
-
def clear_all_strategies(cls, package_name: str) -> None:
|
|
181
|
-
"""Clear all strategies (package and module) for a package."""
|
|
182
|
-
cls.clear_package_strategies(package_name)
|
|
183
|
-
cls.clear_module_strategies(package_name)
|
|
184
|
-
|
|
185
|
-
__all__ = ['StrategyRegistry']
|
|
186
|
-
|