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,506 @@
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
+
46
+ def _normalize_package_name(package_name: Optional[str]) -> str:
47
+ return (package_name or "global").strip().lower()
48
+
49
+
50
+ def _normalize_prefix(prefix: str) -> str:
51
+ prefix = prefix.strip()
52
+ if not prefix:
53
+ return ""
54
+ if not prefix.endswith("."):
55
+ prefix = f"{prefix}."
56
+ return prefix
57
+
58
+
59
+ def _normalize_wrap_hints(values: Iterable[Any]) -> List[str]:
60
+ hints: List[str] = []
61
+ for value in values:
62
+ if value is None:
63
+ continue
64
+ hint = str(value).strip().lower()
65
+ if hint:
66
+ hints.append(hint)
67
+ return hints
68
+
69
+
70
+ # PackageManifest moved to defs.py - import it from there
71
+ from ...defs import PackageManifest
72
+
73
+
74
+ class LazyManifestLoader:
75
+ """
76
+ Loads and caches manifest data per package.
77
+
78
+ Args:
79
+ default_root: Optional fallback root directory used when a package
80
+ root cannot be auto-detected.
81
+ package_roots: Optional explicit mapping used mainly for tests.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ default_root: Optional[Path] = None,
87
+ package_roots: Optional[Dict[str, Path]] = None,
88
+ ) -> None:
89
+ self._default_root = default_root
90
+ self._provided_roots = {
91
+ _normalize_package_name(name): Path(path)
92
+ for name, path in (package_roots or {}).items()
93
+ }
94
+ self._manifest_cache: Dict[str, PackageManifest] = {}
95
+ self._source_signatures: Dict[str, Tuple[str, float, float]] = {}
96
+ self._pyproject_cache: Dict[Path, Tuple[float, Dict[str, Any]]] = {}
97
+ self._shared_dependency_maps: Dict[
98
+ Tuple[str, float, float], Dict[str, Dict[str, str]]
99
+ ] = {}
100
+ self._lock = RLock()
101
+ self._generation = 0
102
+
103
+ @property
104
+ def generation(self) -> int:
105
+ """Incremented whenever any manifest content changes."""
106
+ return self._generation
107
+
108
+ def clear_cache(self) -> None:
109
+ """Forcefully clear cached manifests."""
110
+ with self._lock:
111
+ self._manifest_cache.clear()
112
+ self._source_signatures.clear()
113
+ self._pyproject_cache.clear()
114
+ self._shared_dependency_maps.clear()
115
+ self._generation += 1
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
+
139
+ # --------------------------------------------------------------------- #
140
+ # Public API
141
+ # --------------------------------------------------------------------- #
142
+ def get_manifest(self, package_name: Optional[str]) -> Optional[PackageManifest]:
143
+ """Return manifest for the provided package."""
144
+ key = _normalize_package_name(package_name)
145
+ with self._lock:
146
+ signature = self._compute_signature(key)
147
+ cached_signature = self._source_signatures.get(key)
148
+ if (
149
+ cached_signature is not None
150
+ and signature is not None
151
+ and cached_signature == signature
152
+ and key in self._manifest_cache
153
+ ):
154
+ return self._manifest_cache[key]
155
+
156
+ manifest = self._load_manifest(key)
157
+ if manifest is None:
158
+ # Cache miss is still tracked so we don't re-read files
159
+ self._manifest_cache.pop(key, None)
160
+ self._source_signatures[key] = signature or ("", 0.0, 0.0)
161
+ return None
162
+
163
+ self._manifest_cache[key] = manifest
164
+ if signature is not None:
165
+ self._source_signatures[key] = signature
166
+ per_signature = self._shared_dependency_maps.setdefault(signature, {})
167
+ per_signature[manifest.package] = manifest.dependencies.copy()
168
+ self._generation += 1
169
+ return manifest
170
+
171
+ def get_manifest_signature(self, package_name: Optional[str]) -> Optional[Tuple[str, float, float]]:
172
+ key = _normalize_package_name(package_name)
173
+ with self._lock:
174
+ signature = self._source_signatures.get(key)
175
+ if signature is not None:
176
+ return signature
177
+ signature = self._compute_signature(key)
178
+ if signature is not None:
179
+ self._source_signatures[key] = signature
180
+ return signature
181
+
182
+ def get_shared_dependencies(
183
+ self,
184
+ package_name: Optional[str],
185
+ signature: Optional[Tuple[str, float, float]],
186
+ ) -> Optional[Dict[str, str]]:
187
+ if signature is None:
188
+ return None
189
+ with self._lock:
190
+ package_maps = self._shared_dependency_maps.get(signature)
191
+ if not package_maps:
192
+ return None
193
+ key = _normalize_package_name(package_name)
194
+ return package_maps.get(key)
195
+
196
+ # ------------------------------------------------------------------ #
197
+ # Internal helpers
198
+ # ------------------------------------------------------------------ #
199
+ def _load_manifest(self, package_key: str) -> Optional[PackageManifest]:
200
+ root = self._resolve_project_root(package_key)
201
+ pyproject_path = root / "pyproject.toml"
202
+ pyproject_data = self._load_pyproject(pyproject_path)
203
+ json_data, manifest_path = self._load_json_manifest(root, pyproject_data)
204
+
205
+ data = self._merge_sources(package_key, pyproject_data, json_data)
206
+ if not data["dependencies"] and not data["watched_prefixes"] and not data["async_installs"]:
207
+ return None
208
+
209
+ wrap_prefixes = tuple(data.get("wrap_class_prefixes", ()))
210
+
211
+ manifest = PackageManifest(
212
+ package=package_key,
213
+ dependencies=data["dependencies"],
214
+ watched_prefixes=tuple(
215
+ _normalize_prefix(prefix)
216
+ for prefix in data["watched_prefixes"]
217
+ if _normalize_prefix(prefix)
218
+ ),
219
+ async_installs=bool(data.get("async_installs")),
220
+ async_workers=max(1, int(data.get("async_workers", 1))),
221
+ class_wrap_prefixes=wrap_prefixes,
222
+ metadata={
223
+ "root": str(root),
224
+ "manifest_path": str(manifest_path) if manifest_path else None,
225
+ "wrap_class_prefixes": wrap_prefixes,
226
+ },
227
+ )
228
+ return manifest
229
+
230
+ def _compute_signature(self, package_key: str) -> Optional[Tuple[str, float, float]]:
231
+ root = self._resolve_project_root(package_key)
232
+ pyproject_path = root / "pyproject.toml"
233
+ pyproject_mtime = pyproject_path.stat().st_mtime if pyproject_path.exists() else 0.0
234
+ manifest_path = self._resolve_manifest_path(root, pyproject_path)
235
+ json_mtime = manifest_path.stat().st_mtime if manifest_path and manifest_path.exists() else 0.0
236
+ env_token = os.environ.get(ENV_MANIFEST_PATH, "")
237
+ if not manifest_path and not pyproject_path.exists() and not env_token:
238
+ return None
239
+ return (env_token + str(manifest_path), pyproject_mtime, json_mtime)
240
+
241
+ def _resolve_project_root(self, package_key: str) -> Path:
242
+ if package_key in self._provided_roots:
243
+ return self._provided_roots[package_key]
244
+
245
+ module_candidates: Iterable[str]
246
+ if package_key == "global":
247
+ module_candidates = ()
248
+ else:
249
+ module_candidates = (f"exonware.{package_key}", package_key)
250
+
251
+ for module_name in module_candidates:
252
+ spec = importlib.util.find_spec(module_name)
253
+ if spec and spec.origin:
254
+ origin_path = Path(spec.origin).resolve()
255
+ root = self._walk_to_project_root(origin_path.parent)
256
+ if root:
257
+ self._provided_roots[package_key] = root
258
+ return root
259
+
260
+ if self._default_root:
261
+ return self._default_root
262
+ return Path.cwd()
263
+
264
+ @staticmethod
265
+ def _walk_to_project_root(start: Path) -> Optional[Path]:
266
+ current = start
267
+ while True:
268
+ if (current / "pyproject.toml").exists():
269
+ return current
270
+ parent = current.parent
271
+ if parent == current:
272
+ break
273
+ current = parent
274
+ return None
275
+
276
+ # ------------------------------- #
277
+ # Pyproject helpers
278
+ # ------------------------------- #
279
+ def _load_pyproject(self, path: Path) -> Dict[str, Any]:
280
+ if not path.exists() or tomllib is None:
281
+ return {}
282
+
283
+ cached = self._pyproject_cache.get(path)
284
+ current_mtime = path.stat().st_mtime
285
+ if cached and cached[0] == current_mtime:
286
+ return cached[1]
287
+
288
+ try:
289
+ with path.open("rb") as handle:
290
+ data = tomllib.load(handle)
291
+ except Exception:
292
+ data = {}
293
+
294
+ self._pyproject_cache[path] = (current_mtime, data)
295
+ return data
296
+
297
+ def _extract_pyproject_entry(
298
+ self,
299
+ pyproject_data: Dict[str, Any],
300
+ package_key: str,
301
+ ) -> Dict[str, Any]:
302
+ tool_section = pyproject_data.get("tool", {})
303
+ lazy_section = tool_section.get("xwlazy", {})
304
+ packages = lazy_section.get("packages", {})
305
+ entry = packages.get(package_key, {}) or packages.get(package_key.upper(), {})
306
+
307
+ dependencies = {}
308
+ watched = []
309
+ async_installs = lazy_section.get("async_installs") or entry.get("async_installs")
310
+ async_workers = entry.get("async_workers") or lazy_section.get("async_workers")
311
+ wrap_hints = []
312
+
313
+ global_deps = lazy_section.get("dependencies", {})
314
+ if isinstance(global_deps, dict):
315
+ dependencies.update({str(k): str(v) for k, v in global_deps.items()})
316
+
317
+ if "dependencies" in entry and isinstance(entry["dependencies"], dict):
318
+ dependencies.update({str(k): str(v) for k, v in entry["dependencies"].items()})
319
+
320
+ for key in ("watched-prefixes", "watched_prefixes", "watch"):
321
+ values = entry.get(key) or lazy_section.get(key)
322
+ if isinstance(values, list):
323
+ watched.extend(str(v) for v in values)
324
+
325
+ global_wrap = lazy_section.get("wrap_class_prefixes") or lazy_section.get("wrap_classes")
326
+ if isinstance(global_wrap, list):
327
+ wrap_hints.extend(_normalize_wrap_hints(global_wrap))
328
+ entry_wrap = entry.get("wrap_class_prefixes") or entry.get("wrap_classes")
329
+ if isinstance(entry_wrap, list):
330
+ wrap_hints.extend(_normalize_wrap_hints(entry_wrap))
331
+
332
+ return {
333
+ "dependencies": dependencies,
334
+ "watched_prefixes": watched,
335
+ "async_installs": bool(async_installs),
336
+ "async_workers": async_workers or 1,
337
+ "wrap_class_prefixes": wrap_hints,
338
+ }
339
+
340
+ # ------------------------------- #
341
+ # Manifest helpers
342
+ # ------------------------------- #
343
+ def _resolve_manifest_path(self, root: Path, pyproject_path: Path) -> Optional[Path]:
344
+ env_value = os.environ.get(ENV_MANIFEST_PATH)
345
+ if env_value:
346
+ for raw in env_value.split(os.pathsep):
347
+ candidate = Path(raw).expanduser()
348
+ if candidate.exists():
349
+ return candidate
350
+
351
+ if pyproject_path.exists() and tomllib is not None:
352
+ py_data = self._load_pyproject(pyproject_path)
353
+ tool_section = py_data.get("tool", {}).get("xwlazy", {})
354
+ manifest_path = tool_section.get("manifest") or tool_section.get("manifest_path")
355
+ if manifest_path:
356
+ candidate = (root / manifest_path).resolve()
357
+ if candidate.exists():
358
+ return candidate
359
+
360
+ for filename in DEFAULT_MANIFEST_FILENAMES:
361
+ candidate = root / filename
362
+ if candidate.exists():
363
+ return candidate
364
+
365
+ return None
366
+
367
+ def _load_json_manifest(
368
+ self,
369
+ root: Path,
370
+ pyproject_data: Dict[str, Any],
371
+ ) -> Tuple[Dict[str, Any], Optional[Path]]:
372
+ manifest_path = self._resolve_manifest_path(root, root / "pyproject.toml")
373
+ if not manifest_path:
374
+ return {}, None
375
+
376
+ try:
377
+ with manifest_path.open("r", encoding="utf-8") as handle:
378
+ data = json.load(handle)
379
+ if isinstance(data, dict):
380
+ return data, manifest_path
381
+ except Exception:
382
+ pass
383
+
384
+ return {}, manifest_path
385
+
386
+ def _merge_sources(
387
+ self,
388
+ package_key: str,
389
+ pyproject_data: Dict[str, Any],
390
+ json_data: Dict[str, Any],
391
+ ) -> Dict[str, Any]:
392
+ merged_dependencies: Dict[str, str] = {}
393
+ merged_watched: List[str] = []
394
+ merged_wrap_hints: List[str] = []
395
+
396
+ # Pyproject first (acts as baseline)
397
+ py_entry = self._extract_pyproject_entry(pyproject_data, package_key)
398
+ merged_dependencies.update({k.lower(): v for k, v in py_entry["dependencies"].items()})
399
+ merged_watched.extend(py_entry["watched_prefixes"])
400
+ merged_wrap_hints.extend(_normalize_wrap_hints(py_entry.get("wrap_class_prefixes", [])))
401
+
402
+ async_installs = py_entry.get("async_installs", False)
403
+ async_workers = py_entry.get("async_workers", 1)
404
+
405
+ # JSON global settings
406
+ global_deps = json_data.get("dependencies", {})
407
+ if isinstance(global_deps, dict):
408
+ merged_dependencies.update({str(k).lower(): str(v) for k, v in global_deps.items()})
409
+
410
+ global_watch = json_data.get("watch") or json_data.get("watched_prefixes")
411
+ if isinstance(global_watch, list):
412
+ merged_watched.extend(str(item) for item in global_watch)
413
+ global_wrap = json_data.get("wrap_class_prefixes") or json_data.get("wrap_classes")
414
+ if isinstance(global_wrap, list):
415
+ merged_wrap_hints.extend(_normalize_wrap_hints(global_wrap))
416
+
417
+ global_async = json_data.get("async_installs")
418
+ if global_async is not None:
419
+ async_installs = bool(global_async)
420
+ global_workers = json_data.get("async_workers")
421
+ if global_workers is not None:
422
+ async_workers = global_workers
423
+
424
+ packages_section = json_data.get("packages", {})
425
+ if isinstance(packages_section, dict):
426
+ entry = packages_section.get(package_key) or packages_section.get(package_key.upper())
427
+ if isinstance(entry, dict):
428
+ entry_deps = entry.get("dependencies", {})
429
+ if isinstance(entry_deps, dict):
430
+ merged_dependencies.update({str(k).lower(): str(v) for k, v in entry_deps.items()})
431
+
432
+ entry_watch = entry.get("watched_prefixes") or entry.get("watch")
433
+ if isinstance(entry_watch, list):
434
+ merged_watched.extend(str(item) for item in entry_watch)
435
+ entry_wrap = entry.get("wrap_class_prefixes") or entry.get("wrap_classes")
436
+ if isinstance(entry_wrap, list):
437
+ merged_wrap_hints.extend(_normalize_wrap_hints(entry_wrap))
438
+
439
+ if "async_installs" in entry:
440
+ async_installs = bool(entry["async_installs"])
441
+ if "async_workers" in entry:
442
+ async_workers = entry.get("async_workers", async_workers)
443
+
444
+ seen_wrap: Set[str] = set()
445
+ ordered_wrap_hints: List[str] = []
446
+ for hint in merged_wrap_hints:
447
+ if hint not in seen_wrap:
448
+ seen_wrap.add(hint)
449
+ ordered_wrap_hints.append(hint)
450
+
451
+ return {
452
+ "dependencies": merged_dependencies,
453
+ "watched_prefixes": merged_watched,
454
+ "async_installs": async_installs,
455
+ "async_workers": async_workers,
456
+ "wrap_class_prefixes": ordered_wrap_hints,
457
+ }
458
+
459
+
460
+ _manifest_loader: Optional[LazyManifestLoader] = None
461
+ _manifest_loader_lock = RLock()
462
+
463
+
464
+ def get_manifest_loader() -> LazyManifestLoader:
465
+ """
466
+ Return the process-wide manifest loader instance.
467
+
468
+ Calling this function does not force any manifest to be loaded, but the
469
+ loader keeps shared caches that allow multiple subsystems (dependency mapper,
470
+ installer, hook configuration) to observe manifest changes consistently.
471
+ """
472
+ global _manifest_loader
473
+ with _manifest_loader_lock:
474
+ if _manifest_loader is None:
475
+ _manifest_loader = LazyManifestLoader()
476
+ return _manifest_loader
477
+
478
+
479
+ def refresh_manifest_cache() -> None:
480
+ """Forcefully clear the shared manifest loader cache."""
481
+ loader = get_manifest_loader()
482
+ loader.clear_cache()
483
+
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
+
499
+ __all__ = [
500
+ "PackageManifest",
501
+ "LazyManifestLoader",
502
+ "get_manifest_loader",
503
+ "refresh_manifest_cache",
504
+ "sync_manifest_configuration",
505
+ ]
506
+
@@ -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
+