katalyst-engine 3.0.0__tar.gz → 4.0.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-3.0.0 → katalyst_engine-4.0.0}/.gitignore +7 -0
- katalyst_engine-4.0.0/CHANGELOG.md +32 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/PKG-INFO +1 -1
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/pyproject.toml +1 -1
- katalyst_engine-4.0.0/src/katalyst_engine/bundle/path_loader.py +180 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/schema/definition.py +0 -3
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/schema/registry.py +0 -13
- katalyst_engine-4.0.0/tests/bundle/test_path_loader.py +135 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/schema/test_definition.py +12 -5
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/README.md +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/bundle/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/bundle/discovery.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/bundle/loader.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/bundle/protocol.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/compatibility.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/compositional.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/definitive.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/evolvable.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/identity.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/lifecycle.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/relation.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/core/version.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/discovery/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/discovery/declaration.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/discovery/dispatcher.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/discovery/protocol.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/events/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/events/bus.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/events/event.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/extensions/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/extensions/capability.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/extensions/discovery.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/extensions/effector.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/extensions/provider.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/extensions/registry.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/extensions/trigger.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/model/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/model/manager.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/model/materializer.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/model/node.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/model/query.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/model/store.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/py.typed +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/replication/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/replication/engine.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/replication/job.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/replication/transform.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/resolution/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/resolution/conflict.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/resolution/engine.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/resolution/strategies.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/schema/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/schema/manager.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/schema/versioning.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/snapshot/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/snapshot/diff.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/snapshot/snapshot.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/source/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/source/manifest.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/source/registry.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/source/source.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/toolkit/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/toolkit/file_ops.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/toolkit/rendering.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/bundle/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/bundle/test_discovery.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/bundle/test_loader.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/bundle/test_protocol.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/conftest.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_compatibility.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_compositional.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_definitive.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_evolvable.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_identity.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_init.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_lifecycle.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_relation.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/core/test_version.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/discovery/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/discovery/test_declaration.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/discovery/test_dispatcher.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/events/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/events/test_bus.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/extensions/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/extensions/test_effector.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/extensions/test_provider.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/extensions/test_registry.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/model/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/model/test_materializer.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/model/test_node.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/model/test_query.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/model/test_store.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/replication/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/replication/test_engine.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/resolution/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/resolution/test_engine.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/resolution/test_strategies.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/schema/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/schema/test_manager.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/schema/test_registry.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/schema/test_versioning.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/snapshot/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/snapshot/test_diff.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/snapshot/test_snapshot.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/source/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/source/test_source.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/toolkit/__init__.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/toolkit/test_file_ops.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/tests/toolkit/test_rendering.py +0 -0
- {katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/uv.lock +0 -0
|
@@ -103,3 +103,10 @@ stack-tests/test-results/
|
|
|
103
103
|
# Generated artifacts (rebuild with `just generate-schemas`)
|
|
104
104
|
taxonomy/src/katalyst_taxonomy/engine/generated/
|
|
105
105
|
.worktrees/
|
|
106
|
+
.plan-74-75-76-77.md
|
|
107
|
+
.HANDOFF-scaffolding-wiring.md
|
|
108
|
+
.HANDOFF-2-cicd-migration.md
|
|
109
|
+
.HANDOFF-action-execution-fixes.md
|
|
110
|
+
.HANDOFF-bdd-coverage.md
|
|
111
|
+
.HANDOFF-*.md
|
|
112
|
+
docs/superpowers/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog — katalyst-engine
|
|
2
|
+
|
|
3
|
+
All notable changes to the **katalyst-engine** modeling core are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
This package is released independently from `katalyst-taxonomy` and the other
|
|
9
|
+
workspace packages. Tags use the `engine-vX.Y.Z` prefix.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## [4.0.0] -- 2026-06-16
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
|
|
17
|
+
- **BREAKING: Removed the `scaffoldable` flag from the node-model API.** The `NodeDefinition.scaffoldable` field and the `SchemaRegistry.is_scaffoldable()` and `SchemaRegistry.scaffoldable_kinds()` methods are gone. Scaffolding is no longer an engine-level concept: any node kind may be scaffolded, and the decision is owned entirely by the scaffolding plugin/runtime in `katalyst-taxonomy`. Consumers that called `is_scaffoldable()` / `scaffoldable_kinds()` or read `NodeDefinition.scaffoldable` must drop those calls — scaffolding capability is determined by the taxonomy layer, not the engine.
|
|
18
|
+
|
|
19
|
+
## [3.1.0] -- 2026-06-11
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Path-based bundle loading** (`katalyst_engine.bundle.path_loader`) — load a `Bundle` from an arbitrary filesystem path via `importlib.util.spec_from_file_location`, with an engine-only dependency gate (`enforce_engine_only_deps`) that rejects any declared dependency other than `katalyst-engine` before importing. Supports remote/on-demand bundles installed without modifying the environment.
|
|
23
|
+
|
|
24
|
+
## [3.0.0] -- 2026-06-10
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Baseline for independent per-package releases. No engine source changes since the lockstep `v3.0.0` release; this entry establishes the engine package's own version line going forward.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
> Versions prior to 3.0.0 were released in lockstep with `katalyst-taxonomy`
|
|
32
|
+
> under the umbrella `vX.Y.Z` tags. See the root `CHANGELOG.md` for that history.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: katalyst-engine
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs
|
|
5
5
|
Project-URL: Repository, https://github.com/esimplicityinc/katalyst-taxonomy
|
|
6
6
|
Author: eSimplicity
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Path-based bundle loading + dependency gating.
|
|
2
|
+
|
|
3
|
+
This module is the ONLY place in the engine that imports a bundle from an
|
|
4
|
+
arbitrary filesystem path (via ``importlib.util.spec_from_file_location``)
|
|
5
|
+
rather than from an installed, ``sys.path``-importable distribution.
|
|
6
|
+
|
|
7
|
+
It supports loading "remote" bundles that have been fetched to a local
|
|
8
|
+
directory (PyPI wheel, git checkout, URL tarball) WITHOUT installing them
|
|
9
|
+
into the active environment. Two guardrails apply before any of the
|
|
10
|
+
bundle's own code executes:
|
|
11
|
+
|
|
12
|
+
1. **Declared-dependency gate** (:func:`enforce_engine_only_deps`): the
|
|
13
|
+
fetched artifact's declared dependencies must contain nothing other
|
|
14
|
+
than ``katalyst-engine``. This is checked *before* import.
|
|
15
|
+
2. **Import-time backstop**: an undeclared third-party import surfaces as
|
|
16
|
+
an :class:`ImportError` at load time with a clear message.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import importlib.util
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from katalyst_engine.bundle.protocol import Bundle
|
|
27
|
+
|
|
28
|
+
#: The only distribution a remote bundle may depend on.
|
|
29
|
+
ALLOWED_DEPENDENCIES: frozenset[str] = frozenset({"katalyst-engine"})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BundleDependencyError(ValueError):
|
|
33
|
+
"""Raised when a fetched bundle declares disallowed dependencies."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BundleLoadError(ValueError):
|
|
37
|
+
"""Raised when a fetched bundle cannot be path-imported or is not a Bundle."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_dist_name(requirement: str) -> str:
|
|
41
|
+
"""Extract the bare distribution name from a requirement string.
|
|
42
|
+
|
|
43
|
+
Handles forms like ``"katalyst-engine>=3.0,<4"``, ``"foo[extra]>=1"``,
|
|
44
|
+
``"bar ; python_version>='3.10'"``, and normalizes underscores/case.
|
|
45
|
+
Returns "" for entries that are not real runtime requirements (e.g.
|
|
46
|
+
blank lines or extras-only markers we can't attribute to a dist).
|
|
47
|
+
"""
|
|
48
|
+
req = requirement.strip()
|
|
49
|
+
if not req:
|
|
50
|
+
return ""
|
|
51
|
+
# Drop environment markers (after ';')
|
|
52
|
+
req = req.split(";", 1)[0].strip()
|
|
53
|
+
# Cut at the first version/extras/space specifier character.
|
|
54
|
+
for sep in ("[", "(", "<", ">", "=", "!", "~", " "):
|
|
55
|
+
idx = req.find(sep)
|
|
56
|
+
if idx != -1:
|
|
57
|
+
req = req[:idx]
|
|
58
|
+
return req.strip().replace("_", "-").lower()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def enforce_engine_only_deps(declared_dependencies: list[str]) -> None:
|
|
62
|
+
"""Reject a bundle whose declared deps include anything but the allow-list.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
declared_dependencies: Requirement strings parsed from the fetched
|
|
66
|
+
artifact's metadata (wheel ``Requires-Dist`` or pyproject
|
|
67
|
+
``[project.dependencies]``).
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
BundleDependencyError: If any declared dependency names a
|
|
71
|
+
distribution other than ``katalyst-engine``.
|
|
72
|
+
"""
|
|
73
|
+
allowed = {name.replace("_", "-").lower() for name in ALLOWED_DEPENDENCIES}
|
|
74
|
+
disallowed: list[str] = []
|
|
75
|
+
for raw in declared_dependencies:
|
|
76
|
+
name = _normalize_dist_name(raw)
|
|
77
|
+
if not name:
|
|
78
|
+
continue
|
|
79
|
+
if name not in allowed:
|
|
80
|
+
disallowed.append(raw.strip())
|
|
81
|
+
if disallowed:
|
|
82
|
+
raise BundleDependencyError(
|
|
83
|
+
"Remote bundle declares disallowed dependencies "
|
|
84
|
+
f"{disallowed!r}; bundles may depend only on {sorted(allowed)!r}."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _instantiate(factory: object) -> object:
|
|
89
|
+
"""Apply the DiscoveredBundle.load() instantiation contract.
|
|
90
|
+
|
|
91
|
+
If the attribute is a class or callable, call it with no args; if it is
|
|
92
|
+
already a Bundle instance, return it. Returns ``object`` so the caller's
|
|
93
|
+
``isinstance(..., Bundle)`` check is meaningful.
|
|
94
|
+
"""
|
|
95
|
+
if isinstance(factory, type):
|
|
96
|
+
return factory()
|
|
97
|
+
if callable(factory):
|
|
98
|
+
return factory()
|
|
99
|
+
return factory
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class PathImportTarget:
|
|
104
|
+
"""Where to find a bundle within a fetched directory.
|
|
105
|
+
|
|
106
|
+
``module_file`` is the absolute path to the Python file that defines
|
|
107
|
+
(or re-exports) the bundle. ``attr`` is the attribute name to read from
|
|
108
|
+
the imported module (the Bundle class or factory). If ``attr`` is empty,
|
|
109
|
+
the module object itself is treated as the bundle factory.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
module_file: Path
|
|
113
|
+
attr: str = ""
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def parse(cls, spec: str, *, base_dir: Path) -> PathImportTarget:
|
|
117
|
+
"""Parse an ``import_target`` string ("rel/mod.py:Attr") against base_dir."""
|
|
118
|
+
rel, _, attr = spec.partition(":")
|
|
119
|
+
module_file = (base_dir / rel).resolve()
|
|
120
|
+
return cls(module_file=module_file, attr=attr)
|
|
121
|
+
|
|
122
|
+
def to_spec(self, *, base_dir: Path) -> str:
|
|
123
|
+
"""Render this target back to a relative ``import_target`` string."""
|
|
124
|
+
rel = self.module_file.resolve().relative_to(base_dir.resolve())
|
|
125
|
+
return f"{rel}:{self.attr}" if self.attr else str(rel)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def load_bundle_from_path(target: PathImportTarget) -> Bundle:
|
|
129
|
+
"""Path-import a module and return its Bundle instance.
|
|
130
|
+
|
|
131
|
+
This is the engine's only use of ``spec_from_file_location``. The
|
|
132
|
+
module's parent directory is temporarily placed on ``sys.path`` so the
|
|
133
|
+
bundle's intra-package imports (``import my_bundle.wings``) resolve.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
BundleLoadError: If the file cannot be imported or the resulting
|
|
137
|
+
object does not satisfy the :class:`Bundle` protocol.
|
|
138
|
+
"""
|
|
139
|
+
module_file = target.module_file
|
|
140
|
+
if not module_file.is_file():
|
|
141
|
+
raise BundleLoadError(f"Bundle module file not found: {module_file}")
|
|
142
|
+
|
|
143
|
+
# Derive a stable, unique module name from the file path.
|
|
144
|
+
module_name = f"_katalyst_remote_bundle_{abs(hash(str(module_file)))}"
|
|
145
|
+
package_root = module_file.parent.parent # so "pkg.module" imports work
|
|
146
|
+
added_to_path = False
|
|
147
|
+
if str(package_root) not in sys.path:
|
|
148
|
+
sys.path.insert(0, str(package_root))
|
|
149
|
+
added_to_path = True
|
|
150
|
+
try:
|
|
151
|
+
spec = importlib.util.spec_from_file_location(module_name, module_file)
|
|
152
|
+
if spec is None or spec.loader is None:
|
|
153
|
+
raise BundleLoadError(f"Could not create import spec for {module_file}")
|
|
154
|
+
module = importlib.util.module_from_spec(spec)
|
|
155
|
+
sys.modules[module_name] = module
|
|
156
|
+
try:
|
|
157
|
+
spec.loader.exec_module(module)
|
|
158
|
+
except ImportError as exc:
|
|
159
|
+
raise BundleLoadError(
|
|
160
|
+
f"Failed to import bundle from {module_file}: {exc}. "
|
|
161
|
+
"Remote bundles may depend only on katalyst-engine."
|
|
162
|
+
) from exc
|
|
163
|
+
if target.attr:
|
|
164
|
+
if not hasattr(module, target.attr):
|
|
165
|
+
raise BundleLoadError(
|
|
166
|
+
f"Bundle module {module_file} has no attribute {target.attr!r}"
|
|
167
|
+
)
|
|
168
|
+
factory = getattr(module, target.attr)
|
|
169
|
+
else:
|
|
170
|
+
factory = module
|
|
171
|
+
bundle = _instantiate(factory)
|
|
172
|
+
if not isinstance(bundle, Bundle):
|
|
173
|
+
raise BundleLoadError(
|
|
174
|
+
f"Object loaded from {module_file} (attr={target.attr!r}) "
|
|
175
|
+
"does not satisfy the Bundle protocol."
|
|
176
|
+
)
|
|
177
|
+
return bundle
|
|
178
|
+
finally:
|
|
179
|
+
if added_to_path and str(package_root) in sys.path:
|
|
180
|
+
sys.path.remove(str(package_root))
|
|
@@ -120,9 +120,6 @@ class NodeDefinition(Definitive, frozen=True):
|
|
|
120
120
|
cross_references: tuple[FieldRef, ...] = ()
|
|
121
121
|
"""Typed references from fields on this kind to other node kinds."""
|
|
122
122
|
|
|
123
|
-
scaffoldable: bool = False
|
|
124
|
-
"""Whether instances of this kind support scaffolding (project generation)."""
|
|
125
|
-
|
|
126
123
|
clonable: bool = False
|
|
127
124
|
"""Whether instances of this kind can be cloned."""
|
|
128
125
|
|
|
@@ -168,24 +168,11 @@ class SchemaRegistry:
|
|
|
168
168
|
nd = self._node_defs.get(kind)
|
|
169
169
|
return nd.is_extension if nd else False
|
|
170
170
|
|
|
171
|
-
def is_scaffoldable(self, kind: str) -> bool:
|
|
172
|
-
"""Whether this kind supports project scaffolding."""
|
|
173
|
-
nd = self._node_defs.get(kind)
|
|
174
|
-
return nd.scaffoldable if nd else False
|
|
175
|
-
|
|
176
171
|
def is_clonable(self, kind: str) -> bool:
|
|
177
172
|
"""Whether this kind can be cloned."""
|
|
178
173
|
nd = self._node_defs.get(kind)
|
|
179
174
|
return nd.clonable if nd else False
|
|
180
175
|
|
|
181
|
-
def scaffoldable_kinds(self) -> list[str]:
|
|
182
|
-
"""Return all kinds that support scaffolding, sorted by display_order."""
|
|
183
|
-
return [
|
|
184
|
-
nd.describes_kind
|
|
185
|
-
for nd in sorted(self._node_defs.values(), key=lambda d: d.display_order)
|
|
186
|
-
if nd.scaffoldable
|
|
187
|
-
]
|
|
188
|
-
|
|
189
176
|
def clonable_kinds(self) -> list[str]:
|
|
190
177
|
"""Return all kinds that can be cloned, sorted by display_order."""
|
|
191
178
|
return [
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Tests for path-based bundle loading and the engine-only dependency gate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from katalyst_engine.bundle.path_loader import (
|
|
11
|
+
BundleDependencyError,
|
|
12
|
+
BundleLoadError,
|
|
13
|
+
PathImportTarget,
|
|
14
|
+
enforce_engine_only_deps,
|
|
15
|
+
load_bundle_from_path,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestDependencyGate:
|
|
20
|
+
def test_allows_engine_only(self) -> None:
|
|
21
|
+
enforce_engine_only_deps(["katalyst-engine>=3.0,<4"])
|
|
22
|
+
|
|
23
|
+
def test_allows_empty(self) -> None:
|
|
24
|
+
enforce_engine_only_deps([])
|
|
25
|
+
|
|
26
|
+
def test_normalizes_underscores_and_case(self) -> None:
|
|
27
|
+
enforce_engine_only_deps(["Katalyst_Engine >= 3.0"])
|
|
28
|
+
|
|
29
|
+
def test_rejects_third_party(self) -> None:
|
|
30
|
+
with pytest.raises(BundleDependencyError) as exc:
|
|
31
|
+
enforce_engine_only_deps(["katalyst-engine>=3.0", "pandas>=2.0"])
|
|
32
|
+
assert "pandas" in str(exc.value)
|
|
33
|
+
|
|
34
|
+
def test_ignores_marker_only_extras(self) -> None:
|
|
35
|
+
# A requirement gated entirely by an environment marker still names a
|
|
36
|
+
# disallowed dist, so it should be rejected.
|
|
37
|
+
with pytest.raises(BundleDependencyError):
|
|
38
|
+
enforce_engine_only_deps(["requests; python_version >= '3.10'"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _write_bundle(root: Path, *, deps: str = '"katalyst-engine>=3.0,<4"') -> PathImportTarget:
|
|
42
|
+
pkg = root / "src" / "demo_bundle"
|
|
43
|
+
pkg.mkdir(parents=True)
|
|
44
|
+
(pkg / "__init__.py").write_text("")
|
|
45
|
+
(pkg / "bundle.py").write_text(
|
|
46
|
+
textwrap.dedent(
|
|
47
|
+
"""
|
|
48
|
+
from katalyst_engine.bundle.protocol import BundleManifest
|
|
49
|
+
from katalyst_engine.core.identity import Identity
|
|
50
|
+
from katalyst_engine.core.version import Version
|
|
51
|
+
from katalyst_engine.schema.definition import (
|
|
52
|
+
NodeDefinition, SchemaDefinition, WingDefinition,
|
|
53
|
+
)
|
|
54
|
+
from katalyst_engine.schema.manager import SchemaManager
|
|
55
|
+
|
|
56
|
+
NODE = NodeDefinition(
|
|
57
|
+
identity=Identity(kind="node_definition", name="demo_thing"),
|
|
58
|
+
describes_kind="demo_thing",
|
|
59
|
+
wing="demo",
|
|
60
|
+
display_order=900,
|
|
61
|
+
directory_name="demo_things",
|
|
62
|
+
is_root=True,
|
|
63
|
+
is_leaf=True,
|
|
64
|
+
is_extension=False,
|
|
65
|
+
clonable=False,
|
|
66
|
+
display_label="Demo Thing",
|
|
67
|
+
display_label_plural="Demo Things",
|
|
68
|
+
)
|
|
69
|
+
WING = WingDefinition(
|
|
70
|
+
identity=Identity(kind="wing", name="demo"),
|
|
71
|
+
describes_kind="demo",
|
|
72
|
+
display_order=900,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
class DemoBundle:
|
|
76
|
+
@property
|
|
77
|
+
def manifest(self) -> BundleManifest:
|
|
78
|
+
return BundleManifest(
|
|
79
|
+
identity=Identity(kind="bundle", name="demo"),
|
|
80
|
+
version=Version(major=1),
|
|
81
|
+
provided_kinds=frozenset({"demo_thing"}),
|
|
82
|
+
wing_names=frozenset({"demo"}),
|
|
83
|
+
)
|
|
84
|
+
def node_definitions(self): return [NODE]
|
|
85
|
+
def wing_definitions(self): return [WING]
|
|
86
|
+
def schema_definition(self):
|
|
87
|
+
return SchemaDefinition(
|
|
88
|
+
identity=Identity(kind="schema", name="demo"),
|
|
89
|
+
describes_kind="taxonomy",
|
|
90
|
+
api_version="v2alpha",
|
|
91
|
+
schema_version=Version(major=1),
|
|
92
|
+
node_definitions=(NODE.identity,),
|
|
93
|
+
wing_definitions=(WING.identity,),
|
|
94
|
+
)
|
|
95
|
+
def register(self, manager: SchemaManager) -> None:
|
|
96
|
+
manager.register_schema(
|
|
97
|
+
self.schema_definition(),
|
|
98
|
+
node_definitions=self.node_definitions(),
|
|
99
|
+
wing_definitions=self.wing_definitions(),
|
|
100
|
+
)
|
|
101
|
+
"""
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return PathImportTarget(module_file=pkg / "bundle.py", attr="DemoBundle")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestPathImport:
|
|
108
|
+
def test_loads_valid_bundle(self, tmp_path: Path) -> None:
|
|
109
|
+
target = _write_bundle(tmp_path)
|
|
110
|
+
bundle = load_bundle_from_path(target)
|
|
111
|
+
assert bundle.manifest.identity.name == "demo"
|
|
112
|
+
assert "demo_thing" in bundle.manifest.provided_kinds
|
|
113
|
+
|
|
114
|
+
def test_missing_file_raises(self, tmp_path: Path) -> None:
|
|
115
|
+
with pytest.raises(BundleLoadError):
|
|
116
|
+
load_bundle_from_path(PathImportTarget(module_file=tmp_path / "nope.py", attr="X"))
|
|
117
|
+
|
|
118
|
+
def test_missing_attr_raises(self, tmp_path: Path) -> None:
|
|
119
|
+
target = _write_bundle(tmp_path)
|
|
120
|
+
with pytest.raises(BundleLoadError):
|
|
121
|
+
load_bundle_from_path(PathImportTarget(module_file=target.module_file, attr="Nope"))
|
|
122
|
+
|
|
123
|
+
def test_non_bundle_object_raises(self, tmp_path: Path) -> None:
|
|
124
|
+
mod = tmp_path / "plain.py"
|
|
125
|
+
mod.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
mod.write_text("VALUE = 42\n")
|
|
127
|
+
with pytest.raises(BundleLoadError):
|
|
128
|
+
load_bundle_from_path(PathImportTarget(module_file=mod, attr="VALUE"))
|
|
129
|
+
|
|
130
|
+
def test_import_target_round_trip(self, tmp_path: Path) -> None:
|
|
131
|
+
target = _write_bundle(tmp_path)
|
|
132
|
+
spec = target.to_spec(base_dir=tmp_path)
|
|
133
|
+
parsed = PathImportTarget.parse(spec, base_dir=tmp_path)
|
|
134
|
+
assert parsed.module_file == target.module_file
|
|
135
|
+
assert parsed.attr == target.attr
|
|
@@ -134,22 +134,29 @@ class TestNodeDefinition:
|
|
|
134
134
|
assert restored.describes_kind == nd.describes_kind
|
|
135
135
|
assert len(restored.fields) == len(nd.fields)
|
|
136
136
|
|
|
137
|
-
def
|
|
137
|
+
def test_clonable_default(self) -> None:
|
|
138
138
|
nd = NodeDefinition(
|
|
139
139
|
identity=_id("node_definition", "system"),
|
|
140
140
|
describes_kind="system",
|
|
141
141
|
)
|
|
142
|
-
assert nd.scaffoldable is False
|
|
143
142
|
assert nd.clonable is False
|
|
144
143
|
|
|
145
|
-
def
|
|
144
|
+
def test_no_scaffoldable_field(self) -> None:
|
|
145
|
+
# Scaffolding is a plugin concern — the engine NodeDefinition must NOT
|
|
146
|
+
# carry a scaffoldable flag (regression guard for the extraction).
|
|
147
|
+
nd = NodeDefinition(
|
|
148
|
+
identity=_id("node_definition", "layer"),
|
|
149
|
+
describes_kind="layer",
|
|
150
|
+
)
|
|
151
|
+
assert not hasattr(nd, "scaffoldable")
|
|
152
|
+
assert "scaffoldable" not in type(nd).model_fields
|
|
153
|
+
|
|
154
|
+
def test_clonable_set(self) -> None:
|
|
146
155
|
nd = NodeDefinition(
|
|
147
156
|
identity=_id("node_definition", "layer"),
|
|
148
157
|
describes_kind="layer",
|
|
149
|
-
scaffoldable=True,
|
|
150
158
|
clonable=True,
|
|
151
159
|
)
|
|
152
|
-
assert nd.scaffoldable is True
|
|
153
160
|
assert nd.clonable is True
|
|
154
161
|
|
|
155
162
|
def test_display_label_defaults(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/discovery/declaration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/extensions/capability.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/replication/transform.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{katalyst_engine-3.0.0 → katalyst_engine-4.0.0}/src/katalyst_engine/resolution/strategies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|