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.
- katalyst_engine-2.1.0/.gitignore +104 -0
- katalyst_engine-2.1.0/PKG-INFO +50 -0
- katalyst_engine-2.1.0/README.md +31 -0
- katalyst_engine-2.1.0/pyproject.toml +76 -0
- katalyst_engine-2.1.0/src/katalyst_engine/__init__.py +6 -0
- katalyst_engine-2.1.0/src/katalyst_engine/bundle/__init__.py +30 -0
- katalyst_engine-2.1.0/src/katalyst_engine/bundle/discovery.py +158 -0
- katalyst_engine-2.1.0/src/katalyst_engine/bundle/loader.py +134 -0
- katalyst_engine-2.1.0/src/katalyst_engine/bundle/protocol.py +209 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/__init__.py +62 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/compatibility.py +58 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/compositional.py +103 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/definitive.py +195 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/evolvable.py +89 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/identity.py +95 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/lifecycle.py +62 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/relation.py +151 -0
- katalyst_engine-2.1.0/src/katalyst_engine/core/version.py +203 -0
- katalyst_engine-2.1.0/src/katalyst_engine/discovery/__init__.py +20 -0
- katalyst_engine-2.1.0/src/katalyst_engine/discovery/declaration.py +74 -0
- katalyst_engine-2.1.0/src/katalyst_engine/discovery/dispatcher.py +83 -0
- katalyst_engine-2.1.0/src/katalyst_engine/discovery/protocol.py +69 -0
- katalyst_engine-2.1.0/src/katalyst_engine/events/__init__.py +10 -0
- katalyst_engine-2.1.0/src/katalyst_engine/events/bus.py +102 -0
- katalyst_engine-2.1.0/src/katalyst_engine/events/event.py +82 -0
- katalyst_engine-2.1.0/src/katalyst_engine/extensions/__init__.py +32 -0
- katalyst_engine-2.1.0/src/katalyst_engine/extensions/capability.py +45 -0
- katalyst_engine-2.1.0/src/katalyst_engine/extensions/discovery.py +85 -0
- katalyst_engine-2.1.0/src/katalyst_engine/extensions/effector.py +54 -0
- katalyst_engine-2.1.0/src/katalyst_engine/extensions/provider.py +33 -0
- katalyst_engine-2.1.0/src/katalyst_engine/extensions/registry.py +77 -0
- katalyst_engine-2.1.0/src/katalyst_engine/extensions/trigger.py +64 -0
- katalyst_engine-2.1.0/src/katalyst_engine/model/__init__.py +25 -0
- katalyst_engine-2.1.0/src/katalyst_engine/model/manager.py +85 -0
- katalyst_engine-2.1.0/src/katalyst_engine/model/materializer.py +78 -0
- katalyst_engine-2.1.0/src/katalyst_engine/model/node.py +49 -0
- katalyst_engine-2.1.0/src/katalyst_engine/model/query.py +186 -0
- katalyst_engine-2.1.0/src/katalyst_engine/model/store.py +119 -0
- katalyst_engine-2.1.0/src/katalyst_engine/py.typed +0 -0
- katalyst_engine-2.1.0/src/katalyst_engine/replication/__init__.py +30 -0
- katalyst_engine-2.1.0/src/katalyst_engine/replication/engine.py +104 -0
- katalyst_engine-2.1.0/src/katalyst_engine/replication/job.py +88 -0
- katalyst_engine-2.1.0/src/katalyst_engine/replication/transform.py +111 -0
- katalyst_engine-2.1.0/src/katalyst_engine/resolution/__init__.py +32 -0
- katalyst_engine-2.1.0/src/katalyst_engine/resolution/conflict.py +91 -0
- katalyst_engine-2.1.0/src/katalyst_engine/resolution/engine.py +131 -0
- katalyst_engine-2.1.0/src/katalyst_engine/resolution/strategies.py +122 -0
- katalyst_engine-2.1.0/src/katalyst_engine/schema/__init__.py +35 -0
- katalyst_engine-2.1.0/src/katalyst_engine/schema/definition.py +281 -0
- katalyst_engine-2.1.0/src/katalyst_engine/schema/manager.py +95 -0
- katalyst_engine-2.1.0/src/katalyst_engine/schema/registry.py +367 -0
- katalyst_engine-2.1.0/src/katalyst_engine/schema/versioning.py +115 -0
- katalyst_engine-2.1.0/src/katalyst_engine/snapshot/__init__.py +18 -0
- katalyst_engine-2.1.0/src/katalyst_engine/snapshot/diff.py +94 -0
- katalyst_engine-2.1.0/src/katalyst_engine/snapshot/snapshot.py +111 -0
- katalyst_engine-2.1.0/src/katalyst_engine/source/__init__.py +26 -0
- katalyst_engine-2.1.0/src/katalyst_engine/source/manifest.py +45 -0
- katalyst_engine-2.1.0/src/katalyst_engine/source/registry.py +122 -0
- katalyst_engine-2.1.0/src/katalyst_engine/source/source.py +92 -0
- katalyst_engine-2.1.0/src/katalyst_engine/toolkit/__init__.py +22 -0
- katalyst_engine-2.1.0/src/katalyst_engine/toolkit/file_ops.py +194 -0
- katalyst_engine-2.1.0/src/katalyst_engine/toolkit/rendering.py +58 -0
- katalyst_engine-2.1.0/tests/bundle/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/bundle/test_discovery.py +104 -0
- katalyst_engine-2.1.0/tests/bundle/test_loader.py +165 -0
- katalyst_engine-2.1.0/tests/bundle/test_protocol.py +264 -0
- katalyst_engine-2.1.0/tests/conftest.py +1 -0
- katalyst_engine-2.1.0/tests/core/__init__.py +1 -0
- katalyst_engine-2.1.0/tests/core/test_compatibility.py +101 -0
- katalyst_engine-2.1.0/tests/core/test_compositional.py +182 -0
- katalyst_engine-2.1.0/tests/core/test_definitive.py +277 -0
- katalyst_engine-2.1.0/tests/core/test_evolvable.py +178 -0
- katalyst_engine-2.1.0/tests/core/test_identity.py +164 -0
- katalyst_engine-2.1.0/tests/core/test_init.py +53 -0
- katalyst_engine-2.1.0/tests/core/test_lifecycle.py +88 -0
- katalyst_engine-2.1.0/tests/core/test_relation.py +243 -0
- katalyst_engine-2.1.0/tests/core/test_version.py +239 -0
- katalyst_engine-2.1.0/tests/discovery/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/discovery/test_declaration.py +130 -0
- katalyst_engine-2.1.0/tests/discovery/test_dispatcher.py +155 -0
- katalyst_engine-2.1.0/tests/events/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/events/test_bus.py +160 -0
- katalyst_engine-2.1.0/tests/extensions/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/extensions/test_effector.py +116 -0
- katalyst_engine-2.1.0/tests/extensions/test_provider.py +102 -0
- katalyst_engine-2.1.0/tests/extensions/test_registry.py +120 -0
- katalyst_engine-2.1.0/tests/model/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/model/test_materializer.py +157 -0
- katalyst_engine-2.1.0/tests/model/test_node.py +123 -0
- katalyst_engine-2.1.0/tests/model/test_query.py +193 -0
- katalyst_engine-2.1.0/tests/model/test_store.py +144 -0
- katalyst_engine-2.1.0/tests/replication/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/replication/test_engine.py +205 -0
- katalyst_engine-2.1.0/tests/resolution/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/resolution/test_engine.py +153 -0
- katalyst_engine-2.1.0/tests/resolution/test_strategies.py +163 -0
- katalyst_engine-2.1.0/tests/schema/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/schema/test_definition.py +531 -0
- katalyst_engine-2.1.0/tests/schema/test_manager.py +218 -0
- katalyst_engine-2.1.0/tests/schema/test_registry.py +610 -0
- katalyst_engine-2.1.0/tests/schema/test_versioning.py +117 -0
- katalyst_engine-2.1.0/tests/snapshot/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/snapshot/test_diff.py +249 -0
- katalyst_engine-2.1.0/tests/snapshot/test_snapshot.py +206 -0
- katalyst_engine-2.1.0/tests/source/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/source/test_source.py +228 -0
- katalyst_engine-2.1.0/tests/toolkit/__init__.py +0 -0
- katalyst_engine-2.1.0/tests/toolkit/test_file_ops.py +171 -0
- katalyst_engine-2.1.0/tests/toolkit/test_rendering.py +58 -0
- 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,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)
|