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.
Files changed (90) hide show
  1. exonware/__init__.py +86 -16
  2. exonware/xwlazy/version.py +5 -5
  3. exonware/xwlazy.py +2546 -0
  4. exonware/xwlazy_external_libs.toml +716 -0
  5. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/METADATA +6 -6
  6. exonware_xwlazy-1.0.1.2.dist-info/RECORD +8 -0
  7. exonware/xwlazy/__init__.py +0 -367
  8. exonware/xwlazy/common/__init__.py +0 -47
  9. exonware/xwlazy/common/base.py +0 -56
  10. exonware/xwlazy/common/cache.py +0 -504
  11. exonware/xwlazy/common/logger.py +0 -257
  12. exonware/xwlazy/common/services/__init__.py +0 -72
  13. exonware/xwlazy/common/services/dependency_mapper.py +0 -232
  14. exonware/xwlazy/common/services/install_async_utils.py +0 -165
  15. exonware/xwlazy/common/services/install_cache_utils.py +0 -245
  16. exonware/xwlazy/common/services/keyword_detection.py +0 -283
  17. exonware/xwlazy/common/services/spec_cache.py +0 -165
  18. exonware/xwlazy/common/services/state_manager.py +0 -84
  19. exonware/xwlazy/common/strategies/__init__.py +0 -28
  20. exonware/xwlazy/common/strategies/caching_dict.py +0 -44
  21. exonware/xwlazy/common/strategies/caching_installation.py +0 -88
  22. exonware/xwlazy/common/strategies/caching_lfu.py +0 -66
  23. exonware/xwlazy/common/strategies/caching_lru.py +0 -63
  24. exonware/xwlazy/common/strategies/caching_multitier.py +0 -59
  25. exonware/xwlazy/common/strategies/caching_ttl.py +0 -59
  26. exonware/xwlazy/config.py +0 -193
  27. exonware/xwlazy/contracts.py +0 -1396
  28. exonware/xwlazy/defs.py +0 -378
  29. exonware/xwlazy/errors.py +0 -276
  30. exonware/xwlazy/facade.py +0 -991
  31. exonware/xwlazy/module/__init__.py +0 -18
  32. exonware/xwlazy/module/base.py +0 -565
  33. exonware/xwlazy/module/data.py +0 -17
  34. exonware/xwlazy/module/facade.py +0 -246
  35. exonware/xwlazy/module/importer_engine.py +0 -2117
  36. exonware/xwlazy/module/strategies/__init__.py +0 -22
  37. exonware/xwlazy/module/strategies/module_helper_lazy.py +0 -93
  38. exonware/xwlazy/module/strategies/module_helper_simple.py +0 -65
  39. exonware/xwlazy/module/strategies/module_manager_advanced.py +0 -111
  40. exonware/xwlazy/module/strategies/module_manager_simple.py +0 -95
  41. exonware/xwlazy/package/__init__.py +0 -18
  42. exonware/xwlazy/package/base.py +0 -798
  43. exonware/xwlazy/package/conf.py +0 -324
  44. exonware/xwlazy/package/data.py +0 -17
  45. exonware/xwlazy/package/facade.py +0 -480
  46. exonware/xwlazy/package/services/__init__.py +0 -84
  47. exonware/xwlazy/package/services/async_install_handle.py +0 -87
  48. exonware/xwlazy/package/services/config_manager.py +0 -245
  49. exonware/xwlazy/package/services/discovery.py +0 -370
  50. exonware/xwlazy/package/services/host_packages.py +0 -145
  51. exonware/xwlazy/package/services/install_async.py +0 -277
  52. exonware/xwlazy/package/services/install_cache.py +0 -145
  53. exonware/xwlazy/package/services/install_interactive.py +0 -59
  54. exonware/xwlazy/package/services/install_policy.py +0 -156
  55. exonware/xwlazy/package/services/install_registry.py +0 -54
  56. exonware/xwlazy/package/services/install_result.py +0 -17
  57. exonware/xwlazy/package/services/install_sbom.py +0 -153
  58. exonware/xwlazy/package/services/install_utils.py +0 -79
  59. exonware/xwlazy/package/services/installer_engine.py +0 -406
  60. exonware/xwlazy/package/services/lazy_installer.py +0 -718
  61. exonware/xwlazy/package/services/manifest.py +0 -496
  62. exonware/xwlazy/package/services/strategy_registry.py +0 -186
  63. exonware/xwlazy/package/strategies/__init__.py +0 -57
  64. exonware/xwlazy/package/strategies/package_discovery_file.py +0 -129
  65. exonware/xwlazy/package/strategies/package_discovery_hybrid.py +0 -84
  66. exonware/xwlazy/package/strategies/package_discovery_manifest.py +0 -101
  67. exonware/xwlazy/package/strategies/package_execution_async.py +0 -113
  68. exonware/xwlazy/package/strategies/package_execution_cached.py +0 -90
  69. exonware/xwlazy/package/strategies/package_execution_pip.py +0 -99
  70. exonware/xwlazy/package/strategies/package_execution_wheel.py +0 -106
  71. exonware/xwlazy/package/strategies/package_mapping_discovery_first.py +0 -100
  72. exonware/xwlazy/package/strategies/package_mapping_hybrid.py +0 -105
  73. exonware/xwlazy/package/strategies/package_mapping_manifest_first.py +0 -100
  74. exonware/xwlazy/package/strategies/package_policy_allow_list.py +0 -57
  75. exonware/xwlazy/package/strategies/package_policy_deny_list.py +0 -57
  76. exonware/xwlazy/package/strategies/package_policy_permissive.py +0 -46
  77. exonware/xwlazy/package/strategies/package_timing_clean.py +0 -67
  78. exonware/xwlazy/package/strategies/package_timing_full.py +0 -66
  79. exonware/xwlazy/package/strategies/package_timing_smart.py +0 -68
  80. exonware/xwlazy/package/strategies/package_timing_temporary.py +0 -66
  81. exonware/xwlazy/runtime/__init__.py +0 -18
  82. exonware/xwlazy/runtime/adaptive_learner.py +0 -129
  83. exonware/xwlazy/runtime/base.py +0 -274
  84. exonware/xwlazy/runtime/facade.py +0 -94
  85. exonware/xwlazy/runtime/intelligent_selector.py +0 -170
  86. exonware/xwlazy/runtime/metrics.py +0 -60
  87. exonware/xwlazy/runtime/performance.py +0 -37
  88. exonware_xwlazy-0.1.0.22.dist-info/RECORD +0 -87
  89. {exonware_xwlazy-0.1.0.22.dist-info → exonware_xwlazy-1.0.1.2.dist-info}/WHEEL +0 -0
  90. {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
-