katalyst-engine 2.1.0__tar.gz

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 (110) hide show
  1. katalyst_engine-2.1.0/.gitignore +104 -0
  2. katalyst_engine-2.1.0/PKG-INFO +50 -0
  3. katalyst_engine-2.1.0/README.md +31 -0
  4. katalyst_engine-2.1.0/pyproject.toml +76 -0
  5. katalyst_engine-2.1.0/src/katalyst_engine/__init__.py +6 -0
  6. katalyst_engine-2.1.0/src/katalyst_engine/bundle/__init__.py +30 -0
  7. katalyst_engine-2.1.0/src/katalyst_engine/bundle/discovery.py +158 -0
  8. katalyst_engine-2.1.0/src/katalyst_engine/bundle/loader.py +134 -0
  9. katalyst_engine-2.1.0/src/katalyst_engine/bundle/protocol.py +209 -0
  10. katalyst_engine-2.1.0/src/katalyst_engine/core/__init__.py +62 -0
  11. katalyst_engine-2.1.0/src/katalyst_engine/core/compatibility.py +58 -0
  12. katalyst_engine-2.1.0/src/katalyst_engine/core/compositional.py +103 -0
  13. katalyst_engine-2.1.0/src/katalyst_engine/core/definitive.py +195 -0
  14. katalyst_engine-2.1.0/src/katalyst_engine/core/evolvable.py +89 -0
  15. katalyst_engine-2.1.0/src/katalyst_engine/core/identity.py +95 -0
  16. katalyst_engine-2.1.0/src/katalyst_engine/core/lifecycle.py +62 -0
  17. katalyst_engine-2.1.0/src/katalyst_engine/core/relation.py +151 -0
  18. katalyst_engine-2.1.0/src/katalyst_engine/core/version.py +203 -0
  19. katalyst_engine-2.1.0/src/katalyst_engine/discovery/__init__.py +20 -0
  20. katalyst_engine-2.1.0/src/katalyst_engine/discovery/declaration.py +74 -0
  21. katalyst_engine-2.1.0/src/katalyst_engine/discovery/dispatcher.py +83 -0
  22. katalyst_engine-2.1.0/src/katalyst_engine/discovery/protocol.py +69 -0
  23. katalyst_engine-2.1.0/src/katalyst_engine/events/__init__.py +10 -0
  24. katalyst_engine-2.1.0/src/katalyst_engine/events/bus.py +102 -0
  25. katalyst_engine-2.1.0/src/katalyst_engine/events/event.py +82 -0
  26. katalyst_engine-2.1.0/src/katalyst_engine/extensions/__init__.py +32 -0
  27. katalyst_engine-2.1.0/src/katalyst_engine/extensions/capability.py +45 -0
  28. katalyst_engine-2.1.0/src/katalyst_engine/extensions/discovery.py +85 -0
  29. katalyst_engine-2.1.0/src/katalyst_engine/extensions/effector.py +54 -0
  30. katalyst_engine-2.1.0/src/katalyst_engine/extensions/provider.py +33 -0
  31. katalyst_engine-2.1.0/src/katalyst_engine/extensions/registry.py +77 -0
  32. katalyst_engine-2.1.0/src/katalyst_engine/extensions/trigger.py +64 -0
  33. katalyst_engine-2.1.0/src/katalyst_engine/model/__init__.py +25 -0
  34. katalyst_engine-2.1.0/src/katalyst_engine/model/manager.py +85 -0
  35. katalyst_engine-2.1.0/src/katalyst_engine/model/materializer.py +78 -0
  36. katalyst_engine-2.1.0/src/katalyst_engine/model/node.py +49 -0
  37. katalyst_engine-2.1.0/src/katalyst_engine/model/query.py +186 -0
  38. katalyst_engine-2.1.0/src/katalyst_engine/model/store.py +119 -0
  39. katalyst_engine-2.1.0/src/katalyst_engine/py.typed +0 -0
  40. katalyst_engine-2.1.0/src/katalyst_engine/replication/__init__.py +30 -0
  41. katalyst_engine-2.1.0/src/katalyst_engine/replication/engine.py +104 -0
  42. katalyst_engine-2.1.0/src/katalyst_engine/replication/job.py +88 -0
  43. katalyst_engine-2.1.0/src/katalyst_engine/replication/transform.py +111 -0
  44. katalyst_engine-2.1.0/src/katalyst_engine/resolution/__init__.py +32 -0
  45. katalyst_engine-2.1.0/src/katalyst_engine/resolution/conflict.py +91 -0
  46. katalyst_engine-2.1.0/src/katalyst_engine/resolution/engine.py +131 -0
  47. katalyst_engine-2.1.0/src/katalyst_engine/resolution/strategies.py +122 -0
  48. katalyst_engine-2.1.0/src/katalyst_engine/schema/__init__.py +35 -0
  49. katalyst_engine-2.1.0/src/katalyst_engine/schema/definition.py +281 -0
  50. katalyst_engine-2.1.0/src/katalyst_engine/schema/manager.py +95 -0
  51. katalyst_engine-2.1.0/src/katalyst_engine/schema/registry.py +367 -0
  52. katalyst_engine-2.1.0/src/katalyst_engine/schema/versioning.py +115 -0
  53. katalyst_engine-2.1.0/src/katalyst_engine/snapshot/__init__.py +18 -0
  54. katalyst_engine-2.1.0/src/katalyst_engine/snapshot/diff.py +94 -0
  55. katalyst_engine-2.1.0/src/katalyst_engine/snapshot/snapshot.py +111 -0
  56. katalyst_engine-2.1.0/src/katalyst_engine/source/__init__.py +26 -0
  57. katalyst_engine-2.1.0/src/katalyst_engine/source/manifest.py +45 -0
  58. katalyst_engine-2.1.0/src/katalyst_engine/source/registry.py +122 -0
  59. katalyst_engine-2.1.0/src/katalyst_engine/source/source.py +92 -0
  60. katalyst_engine-2.1.0/src/katalyst_engine/toolkit/__init__.py +22 -0
  61. katalyst_engine-2.1.0/src/katalyst_engine/toolkit/file_ops.py +194 -0
  62. katalyst_engine-2.1.0/src/katalyst_engine/toolkit/rendering.py +58 -0
  63. katalyst_engine-2.1.0/tests/bundle/__init__.py +0 -0
  64. katalyst_engine-2.1.0/tests/bundle/test_discovery.py +104 -0
  65. katalyst_engine-2.1.0/tests/bundle/test_loader.py +165 -0
  66. katalyst_engine-2.1.0/tests/bundle/test_protocol.py +264 -0
  67. katalyst_engine-2.1.0/tests/conftest.py +1 -0
  68. katalyst_engine-2.1.0/tests/core/__init__.py +1 -0
  69. katalyst_engine-2.1.0/tests/core/test_compatibility.py +101 -0
  70. katalyst_engine-2.1.0/tests/core/test_compositional.py +182 -0
  71. katalyst_engine-2.1.0/tests/core/test_definitive.py +277 -0
  72. katalyst_engine-2.1.0/tests/core/test_evolvable.py +178 -0
  73. katalyst_engine-2.1.0/tests/core/test_identity.py +164 -0
  74. katalyst_engine-2.1.0/tests/core/test_init.py +53 -0
  75. katalyst_engine-2.1.0/tests/core/test_lifecycle.py +88 -0
  76. katalyst_engine-2.1.0/tests/core/test_relation.py +243 -0
  77. katalyst_engine-2.1.0/tests/core/test_version.py +239 -0
  78. katalyst_engine-2.1.0/tests/discovery/__init__.py +0 -0
  79. katalyst_engine-2.1.0/tests/discovery/test_declaration.py +130 -0
  80. katalyst_engine-2.1.0/tests/discovery/test_dispatcher.py +155 -0
  81. katalyst_engine-2.1.0/tests/events/__init__.py +0 -0
  82. katalyst_engine-2.1.0/tests/events/test_bus.py +160 -0
  83. katalyst_engine-2.1.0/tests/extensions/__init__.py +0 -0
  84. katalyst_engine-2.1.0/tests/extensions/test_effector.py +116 -0
  85. katalyst_engine-2.1.0/tests/extensions/test_provider.py +102 -0
  86. katalyst_engine-2.1.0/tests/extensions/test_registry.py +120 -0
  87. katalyst_engine-2.1.0/tests/model/__init__.py +0 -0
  88. katalyst_engine-2.1.0/tests/model/test_materializer.py +157 -0
  89. katalyst_engine-2.1.0/tests/model/test_node.py +123 -0
  90. katalyst_engine-2.1.0/tests/model/test_query.py +193 -0
  91. katalyst_engine-2.1.0/tests/model/test_store.py +144 -0
  92. katalyst_engine-2.1.0/tests/replication/__init__.py +0 -0
  93. katalyst_engine-2.1.0/tests/replication/test_engine.py +205 -0
  94. katalyst_engine-2.1.0/tests/resolution/__init__.py +0 -0
  95. katalyst_engine-2.1.0/tests/resolution/test_engine.py +153 -0
  96. katalyst_engine-2.1.0/tests/resolution/test_strategies.py +163 -0
  97. katalyst_engine-2.1.0/tests/schema/__init__.py +0 -0
  98. katalyst_engine-2.1.0/tests/schema/test_definition.py +531 -0
  99. katalyst_engine-2.1.0/tests/schema/test_manager.py +218 -0
  100. katalyst_engine-2.1.0/tests/schema/test_registry.py +610 -0
  101. katalyst_engine-2.1.0/tests/schema/test_versioning.py +117 -0
  102. katalyst_engine-2.1.0/tests/snapshot/__init__.py +0 -0
  103. katalyst_engine-2.1.0/tests/snapshot/test_diff.py +249 -0
  104. katalyst_engine-2.1.0/tests/snapshot/test_snapshot.py +206 -0
  105. katalyst_engine-2.1.0/tests/source/__init__.py +0 -0
  106. katalyst_engine-2.1.0/tests/source/test_source.py +228 -0
  107. katalyst_engine-2.1.0/tests/toolkit/__init__.py +0 -0
  108. katalyst_engine-2.1.0/tests/toolkit/test_file_ops.py +171 -0
  109. katalyst_engine-2.1.0/tests/toolkit/test_rendering.py +58 -0
  110. katalyst_engine-2.1.0/uv.lock +371 -0
@@ -0,0 +1,104 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # PyInstaller
28
+ *.manifest
29
+ *.spec
30
+
31
+ # Installer logs
32
+ pip-log.txt
33
+ pip-delete-this-directory.txt
34
+
35
+ # Unit test / coverage reports
36
+ htmlcov/
37
+ .tox/
38
+ .nox/
39
+ .coverage
40
+ .coverage.*
41
+ .cache
42
+ nosetests.xml
43
+ coverage.xml
44
+ *.cover
45
+ *.py,cover
46
+ .hypothesis/
47
+ .pytest_cache/
48
+
49
+ # Translations
50
+ *.mo
51
+ *.pot
52
+
53
+ # Environments
54
+ .env
55
+ .venv
56
+ env/
57
+ venv/
58
+ ENV/
59
+ env.bak/
60
+ venv.bak/
61
+
62
+ # Spyder project settings
63
+ .spyderproject
64
+ .spyproject
65
+
66
+ # Rope project settings
67
+ .ropeproject
68
+
69
+ # mkdocs documentation
70
+ /site
71
+
72
+ # mypy / pyright
73
+ .mypy_cache/
74
+ .dmypy.json
75
+ dmypy.json
76
+ .pyright/
77
+
78
+ # Ruff
79
+ .ruff_cache/
80
+
81
+ # IDE
82
+ .idea/
83
+ .vscode/
84
+ *.swp
85
+ *.swo
86
+ *~
87
+
88
+ # OS
89
+ .DS_Store
90
+ Thumbs.db
91
+
92
+ # Local configuration
93
+ *.local
94
+
95
+ # Browser/Playwright state files
96
+ browser-state-*.json
97
+
98
+ # Build artifacts
99
+ site/
100
+ stack-tests/cucumber-report/
101
+ stack-tests/test-results/
102
+
103
+ # Generated artifacts (rebuild with `just generate-schemas`)
104
+ taxonomy/src/katalyst_taxonomy/engine/generated/
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: katalyst-engine
3
+ Version: 2.1.0
4
+ Summary: A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs
5
+ Project-URL: Repository, https://github.com/esimplicityinc/katalyst-taxonomy
6
+ Author: eSimplicity
7
+ License: Proprietary
8
+ Keywords: declaration,engine,evolvable,graph,primitives,registry,schema
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: Other/Proprietary License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: pydantic>=2.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # katalyst-engine
21
+
22
+ A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs.
23
+
24
+ ## Status
25
+
26
+ **Alpha** — Engine Phase 1 (`core/` primitives) is implemented.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install katalyst-engine
32
+ ```
33
+
34
+ ## Core Primitives
35
+
36
+ - **Evolvable** — identity + version + lifecycle + compatibility contract
37
+ - **Definitive** — describes the structure/constraints of other Evolvables
38
+ - **Compositional** — groups/contains other Evolvables
39
+ - **Relation** — typed, directed, versioned edge between two Evolvables
40
+
41
+ ```python
42
+ from katalyst_engine.core import Identity, Version, Evolvable, Lifecycle
43
+
44
+ node = Evolvable(
45
+ identity=Identity(kind="system", name="backend", namespace="org"),
46
+ version=Version(major=1),
47
+ lifecycle=Lifecycle.STABLE,
48
+ )
49
+ print(node.fqn) # "org.backend"
50
+ ```
@@ -0,0 +1,31 @@
1
+ # katalyst-engine
2
+
3
+ A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs.
4
+
5
+ ## Status
6
+
7
+ **Alpha** — Engine Phase 1 (`core/` primitives) is implemented.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install katalyst-engine
13
+ ```
14
+
15
+ ## Core Primitives
16
+
17
+ - **Evolvable** — identity + version + lifecycle + compatibility contract
18
+ - **Definitive** — describes the structure/constraints of other Evolvables
19
+ - **Compositional** — groups/contains other Evolvables
20
+ - **Relation** — typed, directed, versioned edge between two Evolvables
21
+
22
+ ```python
23
+ from katalyst_engine.core import Identity, Version, Evolvable, Lifecycle
24
+
25
+ node = Evolvable(
26
+ identity=Identity(kind="system", name="backend", namespace="org"),
27
+ version=Version(major=1),
28
+ lifecycle=Lifecycle.STABLE,
29
+ )
30
+ print(node.fqn) # "org.backend"
31
+ ```
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "katalyst-engine"
3
+ version = "2.1.0"
4
+ description = "A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs"
5
+ readme = "README.md"
6
+ license = { text = "Proprietary" }
7
+ requires-python = ">=3.12"
8
+ authors = [
9
+ { name = "eSimplicity" }
10
+ ]
11
+ keywords = [
12
+ "engine", "primitives", "schema", "evolvable",
13
+ "declaration", "graph", "registry",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: Other/Proprietary License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "pydantic>=2.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/esimplicityinc/katalyst-taxonomy"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/katalyst_engine"]
37
+
38
+ [tool.hatch.build.targets.wheel.sources]
39
+ "src" = ""
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest>=8.2",
44
+ "pytest-cov>=5.0",
45
+ "pytest-asyncio>=0.24",
46
+ "ruff>=0.5",
47
+ "pyright>=1.1",
48
+ ]
49
+
50
+ [tool.pytest.ini_options]
51
+ addopts = ["-ra", "--cov-report=term-missing"]
52
+ testpaths = ["tests"]
53
+ pythonpath = ["src"]
54
+
55
+ [tool.ruff]
56
+ target-version = "py312"
57
+ line-length = 100
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E4", "E7", "E9", "F", "B", "I"]
61
+
62
+ [tool.ruff.format]
63
+ quote-style = "double"
64
+
65
+ [tool.pyright]
66
+ include = ["src", "tests"]
67
+ pythonVersion = "3.12"
68
+ typeCheckingMode = "strict"
69
+
70
+ [tool.coverage.run]
71
+ branch = true
72
+ source = ["src/katalyst_engine"]
73
+
74
+ [tool.coverage.report]
75
+ show_missing = true
76
+ skip_covered = true
@@ -0,0 +1,6 @@
1
+ """katalyst-engine — A distributed declaration framework.
2
+
3
+ Primitives for schema-aware, multi-source, evolvable artifact graphs.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,30 @@
1
+ """Bundle — standard packaging for node type registration.
2
+
3
+ A Bundle is a self-describing unit that contributes node types, wing
4
+ definitions, and schema metadata to the engine. Bundles are discovered
5
+ via Python entry points and registered with a SchemaManager.
6
+ """
7
+
8
+ from katalyst_engine.bundle.discovery import (
9
+ BUNDLE_ENTRY_POINT_GROUP,
10
+ BundleDiscovery,
11
+ DiscoveredBundle,
12
+ )
13
+ from katalyst_engine.bundle.loader import BundleLoader
14
+ from katalyst_engine.bundle.protocol import (
15
+ Bundle,
16
+ BundleManifest,
17
+ get_creation_templates,
18
+ get_scaffold_templates,
19
+ )
20
+
21
+ __all__ = [
22
+ "BUNDLE_ENTRY_POINT_GROUP",
23
+ "Bundle",
24
+ "BundleDiscovery",
25
+ "BundleLoader",
26
+ "BundleManifest",
27
+ "DiscoveredBundle",
28
+ "get_creation_templates",
29
+ "get_scaffold_templates",
30
+ ]
@@ -0,0 +1,158 @@
1
+ """Bundle discovery — find bundles via Python entry points.
2
+
3
+ Scans ``importlib.metadata`` entry points in the
4
+ ``katalyst_engine.bundles`` group to discover installed bundles.
5
+ Each entry point should point to a callable that returns a Bundle
6
+ instance (typically the Bundle class itself).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.metadata
12
+ import logging
13
+ from dataclasses import dataclass, field
14
+
15
+ from katalyst_engine.bundle.protocol import Bundle
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ #: Default entry point group for bundle discovery.
20
+ BUNDLE_ENTRY_POINT_GROUP = "katalyst_engine.bundles"
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class DiscoveredBundle:
25
+ """Metadata about a discovered (but not yet loaded) bundle.
26
+
27
+ Represents the location of a bundle found during entry point
28
+ scanning. The ``load()`` method instantiates the actual Bundle.
29
+ """
30
+
31
+ name: str
32
+ """Entry point name (e.g. "forge")."""
33
+
34
+ entry_point: str
35
+ """Dotted module path (e.g. "katalyst_taxonomy.bundles.forge:ForgeBundle")."""
36
+
37
+ group: str = BUNDLE_ENTRY_POINT_GROUP
38
+ """Entry point group this was discovered from."""
39
+
40
+ def load(self) -> Bundle:
41
+ """Load and instantiate the bundle from its entry point.
42
+
43
+ The entry point must be a callable that returns a Bundle
44
+ instance. If it's a class, it will be instantiated with
45
+ no arguments.
46
+
47
+ Raises:
48
+ ImportError: If the module cannot be imported.
49
+ TypeError: If the loaded object is not a valid Bundle.
50
+ """
51
+ module_path, _, attr_name = self.entry_point.rpartition(":")
52
+ if not attr_name:
53
+ # Entry point format is "module.path" (no colon)
54
+ module_path = self.entry_point
55
+ attr_name = ""
56
+
57
+ module = importlib.import_module(module_path)
58
+ if attr_name:
59
+ factory = getattr(module, attr_name)
60
+ else:
61
+ factory = module
62
+
63
+ # If it's a class, instantiate it; if callable, call it
64
+ if isinstance(factory, type):
65
+ return factory() # type: ignore[return-value]
66
+ if callable(factory):
67
+ return factory() # type: ignore[return-value]
68
+ # If it's already a Bundle instance, return it
69
+ return factory # type: ignore[return-value]
70
+
71
+
72
+ @dataclass(slots=True)
73
+ class BundleDiscovery:
74
+ """Discovers bundles from Python entry points.
75
+
76
+ Scans the ``katalyst_engine.bundles`` entry point group to find
77
+ installed bundle packages. Results are cached after first scan.
78
+
79
+ Usage::
80
+
81
+ discovery = BundleDiscovery()
82
+ discovery.scan() # or scan happens lazily on first access
83
+ for db in discovery.all():
84
+ bundle = db.load()
85
+ bundle.register(schema_manager)
86
+ """
87
+
88
+ _discovered: dict[str, DiscoveredBundle] = field(
89
+ default_factory=lambda: dict[str, DiscoveredBundle]()
90
+ )
91
+ _scanned: bool = False
92
+ group: str = BUNDLE_ENTRY_POINT_GROUP
93
+
94
+ def scan(self) -> None:
95
+ """Scan entry points for bundles.
96
+
97
+ Clears any previous results and re-scans. Uses
98
+ ``importlib.metadata.entry_points()`` to discover bundles.
99
+ """
100
+ self._discovered.clear()
101
+
102
+ try:
103
+ eps = importlib.metadata.entry_points(group=self.group)
104
+ except Exception:
105
+ logger.warning("Failed to scan entry points for group %s", self.group)
106
+ self._scanned = True
107
+ return
108
+
109
+ for ep in eps:
110
+ db = DiscoveredBundle(
111
+ name=ep.name,
112
+ entry_point=ep.value,
113
+ group=self.group,
114
+ )
115
+ self._discovered[ep.name] = db
116
+ logger.debug("Discovered bundle: %s -> %s", ep.name, ep.value)
117
+
118
+ self._scanned = True
119
+
120
+ def all(self) -> list[DiscoveredBundle]:
121
+ """Return all discovered bundles, sorted by name.
122
+
123
+ Triggers a scan if one hasn't happened yet.
124
+ """
125
+ if not self._scanned:
126
+ self.scan()
127
+ return sorted(self._discovered.values(), key=lambda db: db.name)
128
+
129
+ def get(self, name: str) -> DiscoveredBundle | None:
130
+ """Look up a discovered bundle by name.
131
+
132
+ Triggers a scan if one hasn't happened yet.
133
+ """
134
+ if not self._scanned:
135
+ self.scan()
136
+ return self._discovered.get(name)
137
+
138
+ @property
139
+ def count(self) -> int:
140
+ """Number of discovered bundles."""
141
+ if not self._scanned:
142
+ self.scan()
143
+ return len(self._discovered)
144
+
145
+ def add(self, bundle: DiscoveredBundle) -> None:
146
+ """Manually add a discovered bundle (for testing or explicit registration).
147
+
148
+ If no scan has happened yet, marks as scanned to prevent a
149
+ lazy scan from clearing manually-added entries.
150
+ """
151
+ if not self._scanned:
152
+ self._scanned = True
153
+ self._discovered[bundle.name] = bundle
154
+
155
+ def clear(self) -> None:
156
+ """Remove all discovered bundles and reset scan state."""
157
+ self._discovered.clear()
158
+ self._scanned = False
@@ -0,0 +1,134 @@
1
+ """Bundle loader — discover and register all installed bundles.
2
+
3
+ Provides a high-level ``load_bundles()`` function that discovers
4
+ bundles via entry points, loads them, and registers them with
5
+ a SchemaManager. This is the standard startup path for any
6
+ application using the engine.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from dataclasses import dataclass, field
13
+
14
+ from katalyst_engine.bundle.discovery import BundleDiscovery, DiscoveredBundle
15
+ from katalyst_engine.bundle.protocol import Bundle, BundleManifest
16
+ from katalyst_engine.schema.manager import SchemaManager
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class BundleLoader:
23
+ """Loads and registers bundles with a SchemaManager.
24
+
25
+ Manages the lifecycle of bundle discovery → loading → registration.
26
+ Tracks which bundles have been loaded and their manifests.
27
+
28
+ Usage::
29
+
30
+ loader = BundleLoader(schema_manager)
31
+ loader.discover_and_register()
32
+ # Or manually:
33
+ loader.register(my_bundle)
34
+ """
35
+
36
+ manager: SchemaManager
37
+ """The SchemaManager to register bundles with."""
38
+
39
+ _loaded: dict[str, Bundle] = field(default_factory=lambda: dict[str, Bundle]())
40
+ _manifests: dict[str, BundleManifest] = field(
41
+ default_factory=lambda: dict[str, BundleManifest]()
42
+ )
43
+
44
+ def register(self, bundle: Bundle) -> BundleManifest:
45
+ """Register a single bundle with the SchemaManager.
46
+
47
+ Args:
48
+ bundle: A Bundle instance to register.
49
+
50
+ Returns:
51
+ The bundle's manifest.
52
+
53
+ Raises:
54
+ ValueError: If a bundle with the same identity is already loaded.
55
+ """
56
+ manifest = bundle.manifest
57
+ fqn = manifest.identity.fqn
58
+
59
+ if fqn in self._loaded:
60
+ raise ValueError(
61
+ f"Bundle already registered: {fqn!r}. "
62
+ f"Unregister it first or use a different identity."
63
+ )
64
+
65
+ bundle.register(self.manager)
66
+ self._loaded[fqn] = bundle
67
+ self._manifests[fqn] = manifest
68
+
69
+ logger.info(
70
+ "Registered bundle %r: %d kinds, %d wings",
71
+ fqn,
72
+ len(manifest.provided_kinds),
73
+ len(manifest.wing_names),
74
+ )
75
+
76
+ return manifest
77
+
78
+ def discover_and_register(
79
+ self,
80
+ discovery: BundleDiscovery | None = None,
81
+ ) -> list[BundleManifest]:
82
+ """Discover bundles via entry points and register them all.
83
+
84
+ Args:
85
+ discovery: Optional pre-configured BundleDiscovery.
86
+ If None, creates a new one and scans entry points.
87
+
88
+ Returns:
89
+ List of manifests for all successfully registered bundles.
90
+ """
91
+ if discovery is None:
92
+ discovery = BundleDiscovery()
93
+ discovery.scan()
94
+
95
+ manifests: list[BundleManifest] = []
96
+
97
+ for discovered in discovery.all():
98
+ try:
99
+ bundle = discovered.load()
100
+ manifest = self.register(bundle)
101
+ manifests.append(manifest)
102
+ except Exception:
103
+ logger.exception("Failed to load bundle %r", discovered.name)
104
+
105
+ return manifests
106
+
107
+ def register_discovered(self, discovered: DiscoveredBundle) -> BundleManifest:
108
+ """Load a single DiscoveredBundle and register it.
109
+
110
+ Args:
111
+ discovered: A discovered bundle to load and register.
112
+
113
+ Returns:
114
+ The bundle's manifest.
115
+ """
116
+ bundle = discovered.load()
117
+ return self.register(bundle)
118
+
119
+ def get_manifest(self, fqn: str) -> BundleManifest | None:
120
+ """Look up a loaded bundle's manifest by FQN."""
121
+ return self._manifests.get(fqn)
122
+
123
+ def all_manifests(self) -> list[BundleManifest]:
124
+ """Return all loaded bundle manifests, sorted by FQN."""
125
+ return sorted(self._manifests.values(), key=lambda m: m.identity.fqn)
126
+
127
+ def is_loaded(self, fqn: str) -> bool:
128
+ """Check if a bundle with the given FQN is loaded."""
129
+ return fqn in self._loaded
130
+
131
+ @property
132
+ def loaded_count(self) -> int:
133
+ """Number of loaded bundles."""
134
+ return len(self._loaded)