katalyst-engine 5.0.1a2__tar.gz → 5.0.1a4__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-5.0.1a2 → katalyst_engine-5.0.1a4}/PKG-INFO +2 -2
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/pyproject.toml +2 -2
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/__init__.py +2 -2
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/loader.py +57 -2
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/policy.py +29 -33
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/protocol.py +8 -1
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/__init__.py +64 -0
- katalyst_engine-5.0.1a4/src/katalyst_engine/evolution/chain.py +213 -0
- katalyst_engine-5.0.1a4/src/katalyst_engine/evolution/changelog.py +201 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/gate.py +21 -0
- katalyst_engine-5.0.1a4/src/katalyst_engine/evolution/lock.py +228 -0
- katalyst_engine-5.0.1a4/src/katalyst_engine/evolution/ranges.py +241 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/release.py +49 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/definition.py +1 -1
- katalyst_engine-5.0.1a4/src/katalyst_engine/schema_docs/__init__.py +35 -0
- katalyst_engine-5.0.1a4/src/katalyst_engine/schema_docs/reference.py +293 -0
- katalyst_engine-5.0.1a4/src/katalyst_engine/schema_docs/regions.py +107 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/toolkit/linkml.py +72 -2
- katalyst_engine-5.0.1a4/tests/bundle/test_bundle_requires.py +116 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/conftest.py +0 -7
- katalyst_engine-5.0.1a4/tests/evolution/test_chain.py +99 -0
- katalyst_engine-5.0.1a4/tests/evolution/test_changelog.py +137 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/test_diff.py +2 -2
- katalyst_engine-5.0.1a4/tests/evolution/test_lock.py +94 -0
- katalyst_engine-5.0.1a4/tests/evolution/test_ranges.py +106 -0
- katalyst_engine-5.0.1a4/tests/evolution/test_regions.py +78 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/test_release.py +50 -0
- katalyst_engine-5.0.1a4/tests/evolution/test_schema_docs.py +161 -0
- katalyst_engine-5.0.1a4/tests/toolkit/test_core_parent.py +171 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/.gitignore +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/CHANGELOG.md +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/README.md +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/discovery.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/path_loader.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/compatibility.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/compositional.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/definitive.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/errors.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/evolvable.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/identity.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/lifecycle.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/matching.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/relation.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/version.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/discovery/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/discovery/declaration.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/discovery/dispatcher.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/discovery/protocol.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/events/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/events/bus.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/events/event.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/diff.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/migration.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/capability.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/discovery.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/effector.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/provider.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/trigger.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/manager.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/materializer.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/node.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/query.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/store.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/py.typed +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/replication/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/replication/engine.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/replication/job.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/replication/transform.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/resolution/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/resolution/conflict.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/resolution/engine.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/resolution/strategies.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/conflict.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/manager.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/versioning.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/snapshot/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/snapshot/diff.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/snapshot/snapshot.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/source/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/source/manifest.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/source/registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/source/source.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/toolkit/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/toolkit/file_ops.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/toolkit/rendering.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_discovery.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_loader.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_loader_policy.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_path_loader.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_protocol.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/conftest.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_compatibility.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_compositional.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_definitive.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_evolvable.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_identity.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_init.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_lifecycle.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_matching.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_matching_consolidation.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_relation.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_relation_selectors.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_version.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/discovery/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/discovery/test_declaration.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/discovery/test_dispatcher.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/events/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/events/test_bus.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/test_gate.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/test_migration.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/extensions/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/extensions/test_effector.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/extensions/test_provider.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/extensions/test_registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/test_materializer.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/test_node.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/test_query.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/test_store.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/replication/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/replication/test_engine.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/resolution/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/resolution/test_engine.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/resolution/test_strategies.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_definition.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_engine_boundary_guard.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_field_role.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_fieldspec_allowed_values.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_manager.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_plugin_settings.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_registry_provenance.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_storage_policy.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_versioning.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/snapshot/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/snapshot/test_diff.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/snapshot/test_snapshot.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/source/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/source/test_source.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/test_version.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/toolkit/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/toolkit/test_file_ops.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/toolkit/test_linkml_bundle.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/toolkit/test_rendering.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: katalyst-engine
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.1a4
|
|
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
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Typing :: Typed
|
|
16
16
|
Requires-Python: >=3.12
|
|
17
|
-
Requires-Dist: katalyst-core<1,>=0.
|
|
17
|
+
Requires-Dist: katalyst-core<1,>=0.2
|
|
18
18
|
Requires-Dist: pydantic>=2.0
|
|
19
19
|
Requires-Dist: pyyaml>=6
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "katalyst-engine"
|
|
3
|
-
version = "5.0.
|
|
3
|
+
version = "5.0.1a4"
|
|
4
4
|
description = "A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Proprietary" }
|
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
|
25
25
|
"pydantic>=2.0",
|
|
26
26
|
# LinkML is the only schema source: bundles build their definitions from the
|
|
27
27
|
# LinkML modules katalyst-core opens (toolkit/linkml.py).
|
|
28
|
-
"katalyst-core>=0.
|
|
28
|
+
"katalyst-core>=0.2,<1",
|
|
29
29
|
"pyyaml>=6",
|
|
30
30
|
]
|
|
31
31
|
|
|
@@ -13,8 +13,8 @@ from katalyst_engine.bundle.discovery import (
|
|
|
13
13
|
from katalyst_engine.bundle.loader import BundleLoader
|
|
14
14
|
from katalyst_engine.bundle.policy import (
|
|
15
15
|
BundleLoadPolicy,
|
|
16
|
+
BundleRequirementError,
|
|
16
17
|
BundleVersionConflictError,
|
|
17
|
-
constraint_is_satisfied,
|
|
18
18
|
)
|
|
19
19
|
from katalyst_engine.bundle.protocol import (
|
|
20
20
|
Bundle,
|
|
@@ -30,9 +30,9 @@ __all__ = [
|
|
|
30
30
|
"BundleLoadPolicy",
|
|
31
31
|
"BundleLoader",
|
|
32
32
|
"BundleManifest",
|
|
33
|
+
"BundleRequirementError",
|
|
33
34
|
"BundleVersionConflictError",
|
|
34
35
|
"DiscoveredBundle",
|
|
35
|
-
"constraint_is_satisfied",
|
|
36
36
|
"get_creation_templates",
|
|
37
37
|
"get_scaffold_templates",
|
|
38
38
|
]
|
|
@@ -10,7 +10,9 @@ Loading is governed by a :class:`~katalyst_engine.bundle.policy.BundleLoadPolicy
|
|
|
10
10
|
:attr:`BundleManifest.version` the moment the manifest is read, provenance is
|
|
11
11
|
recorded so the registry can attribute every kind to a bundle, and
|
|
12
12
|
``BundleManifest.entry_point`` is populated for bundles that arrived over an
|
|
13
|
-
entry point so provenance is reportable.
|
|
13
|
+
entry point so provenance is reportable. Once every bundle is registered, each one's
|
|
14
|
+
declared ranges (:attr:`BundleManifest.requires`) are checked against katalyst-core and
|
|
15
|
+
the other loaded bundles (:meth:`BundleLoader.check_requirements`).
|
|
14
16
|
"""
|
|
15
17
|
|
|
16
18
|
from __future__ import annotations
|
|
@@ -19,9 +21,10 @@ import logging
|
|
|
19
21
|
from dataclasses import dataclass, field
|
|
20
22
|
|
|
21
23
|
from katalyst_engine.bundle.discovery import BundleDiscovery, DiscoveredBundle
|
|
22
|
-
from katalyst_engine.bundle.policy import BundleLoadPolicy
|
|
24
|
+
from katalyst_engine.bundle.policy import BundleLoadPolicy, BundleRequirementError
|
|
23
25
|
from katalyst_engine.bundle.protocol import Bundle, BundleManifest
|
|
24
26
|
from katalyst_engine.core.errors import EngineConfigurationError
|
|
27
|
+
from katalyst_engine.core.version import Version
|
|
25
28
|
from katalyst_engine.schema.manager import SchemaManager
|
|
26
29
|
|
|
27
30
|
logger = logging.getLogger(__name__)
|
|
@@ -107,6 +110,7 @@ class BundleLoader:
|
|
|
107
110
|
manifest = manifest.model_copy(update={"entry_point": entry_point})
|
|
108
111
|
|
|
109
112
|
provider = name or discovered_name or fqn
|
|
113
|
+
self._register_core_relationships()
|
|
110
114
|
with self.manager.registry.providing(provider):
|
|
111
115
|
bundle.register(self.manager)
|
|
112
116
|
|
|
@@ -122,6 +126,19 @@ class BundleLoader:
|
|
|
122
126
|
|
|
123
127
|
return manifest
|
|
124
128
|
|
|
129
|
+
def _register_core_relationships(self) -> None:
|
|
130
|
+
"""katalyst-core's relationship types (``child_of``), once and unattributed.
|
|
131
|
+
|
|
132
|
+
Every bundle uses them and none defines them, so they belong to no provider and
|
|
133
|
+
never conflict; a view narrowed to some bundles keeps them.
|
|
134
|
+
"""
|
|
135
|
+
from katalyst_engine.toolkit.linkml import core_relationships
|
|
136
|
+
|
|
137
|
+
registry = self.manager.registry
|
|
138
|
+
for rel_def in core_relationships():
|
|
139
|
+
if registry.get_relationship(rel_def.describes_kind) is None:
|
|
140
|
+
registry.register_relationship(rel_def)
|
|
141
|
+
|
|
125
142
|
def discover_and_register(
|
|
126
143
|
self,
|
|
127
144
|
discovery: BundleDiscovery | None = None,
|
|
@@ -177,8 +194,46 @@ class BundleLoader:
|
|
|
177
194
|
) from exc
|
|
178
195
|
logger.exception("Failed to load bundle %r", discovered.name)
|
|
179
196
|
|
|
197
|
+
self.check_requirements()
|
|
180
198
|
return manifests
|
|
181
199
|
|
|
200
|
+
def loaded_versions(self) -> dict[str, Version]:
|
|
201
|
+
"""katalyst-core's version and each loaded bundle's, by schema name."""
|
|
202
|
+
from katalyst_engine.evolution.release import core_release
|
|
203
|
+
|
|
204
|
+
core = core_release()
|
|
205
|
+
versions = {core.name: core.version}
|
|
206
|
+
for manifest in self._manifests.values():
|
|
207
|
+
versions[manifest.identity.name] = manifest.version
|
|
208
|
+
return versions
|
|
209
|
+
|
|
210
|
+
def check_requirements(self) -> None:
|
|
211
|
+
"""Refuse a loaded bundle whose declared ranges the loaded schemas don't meet.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
BundleRequirementError: For the first such bundle (by FQN), naming each range
|
|
215
|
+
and the loaded version.
|
|
216
|
+
"""
|
|
217
|
+
from katalyst_engine.evolution.ranges import (
|
|
218
|
+
SchemaRangeError,
|
|
219
|
+
check_requirements,
|
|
220
|
+
parse_requirements,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
loaded = self.loaded_versions()
|
|
224
|
+
for fqn in sorted(self._manifests):
|
|
225
|
+
manifest = self._manifests[fqn]
|
|
226
|
+
if not manifest.requires:
|
|
227
|
+
continue
|
|
228
|
+
name = manifest.identity.name
|
|
229
|
+
try:
|
|
230
|
+
ranges = parse_requirements(dict(manifest.requires), where="requires")
|
|
231
|
+
except SchemaRangeError as exc:
|
|
232
|
+
raise BundleRequirementError(name, (), detail=str(exc)) from exc
|
|
233
|
+
unsatisfied = check_requirements(ranges, loaded)
|
|
234
|
+
if unsatisfied:
|
|
235
|
+
raise BundleRequirementError(name, unsatisfied)
|
|
236
|
+
|
|
182
237
|
def register_discovered(self, discovered: DiscoveredBundle) -> BundleManifest:
|
|
183
238
|
"""Load a single DiscoveredBundle and register it.
|
|
184
239
|
|
|
@@ -10,18 +10,9 @@ Deliberately in the engine and deliberately free of any notion of a profile, a
|
|
|
10
10
|
config document or a filesystem: it is a mapping from bundle name to constraint
|
|
11
11
|
plus a conflict policy, and nothing else.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Anything.
|
|
17
|
-
``^1.2.0``
|
|
18
|
-
Semver-compatible, delegated to :meth:`Version.is_compatible_with` so the
|
|
19
|
-
"0.x is only compatible with itself" rule is stated in exactly one place.
|
|
20
|
-
``>=1.0.0,<2.0.0`` / ``==1.0.0`` / ``>1.0`` …
|
|
21
|
-
Delegated to :meth:`VersionRange.parse`.
|
|
22
|
-
``0.1.0``
|
|
23
|
-
A bare version is an exact pin — the spelling ``spec.schemas[].version``
|
|
24
|
-
uses in the acceptance specs.
|
|
13
|
+
A constraint is a schema range: the one grammar and check of
|
|
14
|
+
:mod:`katalyst_engine.evolution.ranges` (``^1.2``, ``>=1.0,<2``, ``*``; a bare full version
|
|
15
|
+
such as ``0.1.0`` is an exact pin, the spelling ``spec.schemas[].version`` uses).
|
|
25
16
|
"""
|
|
26
17
|
|
|
27
18
|
from __future__ import annotations
|
|
@@ -30,13 +21,14 @@ from dataclasses import dataclass, field
|
|
|
30
21
|
from typing import Mapping
|
|
31
22
|
|
|
32
23
|
from katalyst_engine.core.errors import EngineConfigurationError
|
|
33
|
-
from katalyst_engine.core.version import Version
|
|
24
|
+
from katalyst_engine.core.version import Version
|
|
25
|
+
from katalyst_engine.evolution.ranges import SchemaRange, SchemaRangeError, Unsatisfied, refusal
|
|
34
26
|
from katalyst_engine.schema.conflict import ConflictPolicy
|
|
35
27
|
|
|
36
28
|
__all__ = [
|
|
37
29
|
"BundleLoadPolicy",
|
|
30
|
+
"BundleRequirementError",
|
|
38
31
|
"BundleVersionConflictError",
|
|
39
|
-
"constraint_is_satisfied",
|
|
40
32
|
]
|
|
41
33
|
|
|
42
34
|
|
|
@@ -60,26 +52,30 @@ class BundleVersionConflictError(EngineConfigurationError):
|
|
|
60
52
|
)
|
|
61
53
|
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
"""
|
|
65
|
-
|
|
55
|
+
class BundleRequirementError(EngineConfigurationError):
|
|
56
|
+
"""A loaded bundle's declared range (:attr:`BundleManifest.requires`) is not met.
|
|
57
|
+
|
|
58
|
+
The message names the bundle, each range and the loaded version (or that the schema is
|
|
59
|
+
not loaded): ``schema bundle 'x' supports katalyst-core ^0.2, but katalyst-core 1.0.0
|
|
60
|
+
is loaded``.
|
|
61
|
+
"""
|
|
66
62
|
|
|
63
|
+
def __init__(self, name: str, unsatisfied: tuple[Unsatisfied, ...], detail: str = "") -> None:
|
|
64
|
+
self.name = name
|
|
65
|
+
self.unsatisfied = unsatisfied
|
|
66
|
+
if unsatisfied:
|
|
67
|
+
message = (
|
|
68
|
+
refusal(f"schema bundle {name!r}", unsatisfied)
|
|
69
|
+
+ ": install versions it supports, or a release of it that supports these"
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
message = f"schema bundle {name!r}: {detail}"
|
|
73
|
+
super().__init__(message)
|
|
67
74
|
|
|
68
|
-
def constraint_is_satisfied(constraint: str, version: Version) -> bool:
|
|
69
|
-
"""Whether *version* satisfies *constraint*.
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"""
|
|
75
|
-
text = constraint.strip()
|
|
76
|
-
if not text or text == "*":
|
|
77
|
-
return True
|
|
78
|
-
if text.startswith("^"):
|
|
79
|
-
return version.is_compatible_with(Version.parse(text[1:]))
|
|
80
|
-
if text[0] in "<>=":
|
|
81
|
-
return VersionRange.parse(text).contains(version)
|
|
82
|
-
return version == Version.parse(text)
|
|
76
|
+
def _empty_constraints() -> Mapping[str, str]:
|
|
77
|
+
"""Typed empty-mapping factory — a bare ``lambda: {}`` is partially unknown."""
|
|
78
|
+
return {}
|
|
83
79
|
|
|
84
80
|
|
|
85
81
|
@dataclass(frozen=True, slots=True)
|
|
@@ -135,8 +131,8 @@ class BundleLoadPolicy:
|
|
|
135
131
|
return
|
|
136
132
|
label = next((name for name in names if name), "<unnamed>")
|
|
137
133
|
try:
|
|
138
|
-
satisfied =
|
|
139
|
-
except
|
|
134
|
+
satisfied = SchemaRange.parse(constraint).contains(version)
|
|
135
|
+
except SchemaRangeError as exc:
|
|
140
136
|
raise BundleVersionConflictError(
|
|
141
137
|
label, constraint, version, detail=f"unparseable constraint: {exc}"
|
|
142
138
|
) from exc
|
|
@@ -19,10 +19,11 @@ Bundles may optionally declare templates:
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
+
from collections.abc import Mapping
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from typing import Any, Protocol, cast, runtime_checkable
|
|
24
25
|
|
|
25
|
-
from pydantic import BaseModel
|
|
26
|
+
from pydantic import BaseModel, Field
|
|
26
27
|
|
|
27
28
|
from katalyst_engine.core.identity import Identity
|
|
28
29
|
from katalyst_engine.core.version import Version
|
|
@@ -64,6 +65,12 @@ class BundleManifest(BaseModel, frozen=True):
|
|
|
64
65
|
description: str = ""
|
|
65
66
|
"""Human-readable description of what this bundle provides."""
|
|
66
67
|
|
|
68
|
+
requires: Mapping[str, str] = Field(default_factory=dict[str, str])
|
|
69
|
+
"""The other schemas this bundle builds on, and the versions of each it supports: a
|
|
70
|
+
schema name (``katalyst-core``, another bundle's manifest name) to a range
|
|
71
|
+
(:mod:`katalyst_engine.evolution.ranges`), e.g. ``{"katalyst-core": "^0.2"}``. The
|
|
72
|
+
loader refuses the bundle when a loaded schema is outside its range, or not loaded."""
|
|
73
|
+
|
|
67
74
|
entry_point: str = ""
|
|
68
75
|
"""The entry point string that loaded this bundle (if discovered)."""
|
|
69
76
|
|
{katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/__init__.py
RENAMED
|
@@ -7,8 +7,29 @@ its version bump and its migration.
|
|
|
7
7
|
- :mod:`.migration`: the migration file (``migrations/<from>-to-<to>.yaml``) and the
|
|
8
8
|
pure function that applies its steps to documents in memory.
|
|
9
9
|
- :mod:`.gate`: the version gate CI runs (``kata core schema check``).
|
|
10
|
+
- :mod:`.lock`: the versions a backend records (its schema lock), against the loaded ones.
|
|
11
|
+
- :mod:`.changelog`: a release's schema changes as markdown, from the diff and the migration.
|
|
12
|
+
- :mod:`.chain`: the chain of migrations from a recorded version to the loaded one, run
|
|
13
|
+
a step at a time.
|
|
14
|
+
- :mod:`.ranges`: the schema ranges a dependent declares (``{name: "^9"}``), and the one
|
|
15
|
+
check that refuses it when the loaded schemas don't satisfy them.
|
|
10
16
|
"""
|
|
11
17
|
|
|
18
|
+
from katalyst_engine.evolution.chain import (
|
|
19
|
+
ChainPlan,
|
|
20
|
+
MigrationChainError,
|
|
21
|
+
SchemaChain,
|
|
22
|
+
StepOutcome,
|
|
23
|
+
describe_step,
|
|
24
|
+
migration_chain,
|
|
25
|
+
release_migrations,
|
|
26
|
+
run_chains,
|
|
27
|
+
)
|
|
28
|
+
from katalyst_engine.evolution.changelog import (
|
|
29
|
+
SchemaChangelog,
|
|
30
|
+
build_changelog,
|
|
31
|
+
render_changelog,
|
|
32
|
+
)
|
|
12
33
|
from katalyst_engine.evolution.diff import (
|
|
13
34
|
Category,
|
|
14
35
|
Impact,
|
|
@@ -18,6 +39,15 @@ from katalyst_engine.evolution.diff import (
|
|
|
18
39
|
diff_modules,
|
|
19
40
|
)
|
|
20
41
|
from katalyst_engine.evolution.gate import GateReport, check_release, migration_skeleton
|
|
42
|
+
from katalyst_engine.evolution.lock import (
|
|
43
|
+
LOCK_FILE_NAME,
|
|
44
|
+
BundleStatus,
|
|
45
|
+
SchemaLock,
|
|
46
|
+
SchemaLockError,
|
|
47
|
+
SchemaState,
|
|
48
|
+
SchemaStatus,
|
|
49
|
+
compare_lock,
|
|
50
|
+
)
|
|
21
51
|
from katalyst_engine.evolution.migration import (
|
|
22
52
|
CODE_HOOK_GROUP,
|
|
23
53
|
Document,
|
|
@@ -28,7 +58,16 @@ from katalyst_engine.evolution.migration import (
|
|
|
28
58
|
apply_migration,
|
|
29
59
|
load_migration,
|
|
30
60
|
)
|
|
61
|
+
from katalyst_engine.evolution.ranges import (
|
|
62
|
+
SchemaRange,
|
|
63
|
+
SchemaRangeError,
|
|
64
|
+
Unsatisfied,
|
|
65
|
+
check_requirements,
|
|
66
|
+
parse_requirements,
|
|
67
|
+
refusal,
|
|
68
|
+
)
|
|
31
69
|
from katalyst_engine.evolution.release import (
|
|
70
|
+
ArchiveState,
|
|
32
71
|
SchemaRelease,
|
|
33
72
|
SchemaVersionMismatchError,
|
|
34
73
|
bundle_release,
|
|
@@ -38,25 +77,50 @@ from katalyst_engine.evolution.release import (
|
|
|
38
77
|
|
|
39
78
|
__all__ = [
|
|
40
79
|
"CODE_HOOK_GROUP",
|
|
80
|
+
"LOCK_FILE_NAME",
|
|
81
|
+
"ArchiveState",
|
|
82
|
+
"BundleStatus",
|
|
41
83
|
"Category",
|
|
84
|
+
"ChainPlan",
|
|
42
85
|
"Document",
|
|
43
86
|
"DocumentSet",
|
|
44
87
|
"GateReport",
|
|
45
88
|
"Impact",
|
|
46
89
|
"Location",
|
|
47
90
|
"Migration",
|
|
91
|
+
"MigrationChainError",
|
|
48
92
|
"MigrationFileError",
|
|
49
93
|
"MigrationStepError",
|
|
94
|
+
"SchemaChain",
|
|
50
95
|
"SchemaChange",
|
|
96
|
+
"SchemaChangelog",
|
|
51
97
|
"SchemaDiff",
|
|
98
|
+
"SchemaLock",
|
|
99
|
+
"SchemaLockError",
|
|
100
|
+
"SchemaRange",
|
|
101
|
+
"SchemaRangeError",
|
|
52
102
|
"SchemaRelease",
|
|
103
|
+
"SchemaState",
|
|
104
|
+
"SchemaStatus",
|
|
53
105
|
"SchemaVersionMismatchError",
|
|
106
|
+
"StepOutcome",
|
|
107
|
+
"Unsatisfied",
|
|
54
108
|
"apply_migration",
|
|
109
|
+
"build_changelog",
|
|
55
110
|
"bundle_release",
|
|
56
111
|
"check_release",
|
|
112
|
+
"check_requirements",
|
|
113
|
+
"compare_lock",
|
|
57
114
|
"core_release",
|
|
115
|
+
"describe_step",
|
|
58
116
|
"diff_modules",
|
|
59
117
|
"installed_releases",
|
|
60
118
|
"load_migration",
|
|
119
|
+
"migration_chain",
|
|
61
120
|
"migration_skeleton",
|
|
121
|
+
"parse_requirements",
|
|
122
|
+
"refusal",
|
|
123
|
+
"release_migrations",
|
|
124
|
+
"render_changelog",
|
|
125
|
+
"run_chains",
|
|
62
126
|
]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Moving documents across releases: the chain of migrations from the version a backend
|
|
2
|
+
is at to the loaded one, and applying it step by step with what each step changed.
|
|
3
|
+
|
|
4
|
+
A schema's migrations live beside its modules (``migrations/<from>-to-<to>.yaml``), one
|
|
5
|
+
per breaking release, each from the release before it. The chain from a version is the
|
|
6
|
+
migration out of its major, then the one out of the next, until the loaded major:
|
|
7
|
+
8.x to 10.x runs 8 → 9, then 9 → 10. Within a major nothing is migrated.
|
|
8
|
+
|
|
9
|
+
:func:`run_chains` applies the chains of several schemas to one set of documents, in
|
|
10
|
+
order, a step at a time, so a dry run can say what each step did. It is pure: writing
|
|
11
|
+
the result back to a backend is the caller's.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import copy
|
|
17
|
+
import json
|
|
18
|
+
from collections import Counter
|
|
19
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from katalyst_engine.core.version import Version
|
|
24
|
+
from katalyst_engine.evolution.migration import (
|
|
25
|
+
CodeStep,
|
|
26
|
+
DeleteKind,
|
|
27
|
+
Document,
|
|
28
|
+
DocumentSet,
|
|
29
|
+
DropField,
|
|
30
|
+
MapEnum,
|
|
31
|
+
Migration,
|
|
32
|
+
MoveField,
|
|
33
|
+
MoveFiles,
|
|
34
|
+
RenameField,
|
|
35
|
+
RenameKind,
|
|
36
|
+
RenameRelationship,
|
|
37
|
+
Reparent,
|
|
38
|
+
SetDefault,
|
|
39
|
+
Step,
|
|
40
|
+
apply_migration,
|
|
41
|
+
load_migration,
|
|
42
|
+
)
|
|
43
|
+
from katalyst_engine.evolution.release import SchemaRelease
|
|
44
|
+
from katalyst_engine.schema.definition import NodeDefinition
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"ChainPlan",
|
|
48
|
+
"MigrationChainError",
|
|
49
|
+
"SchemaChain",
|
|
50
|
+
"StepOutcome",
|
|
51
|
+
"describe_step",
|
|
52
|
+
"migration_chain",
|
|
53
|
+
"release_migrations",
|
|
54
|
+
"run_chains",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MigrationChainError(ValueError):
|
|
59
|
+
"""No chain of migrations leads from a backend's version to the loaded one."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def release_migrations(release: SchemaRelease) -> tuple[Migration, ...]:
|
|
63
|
+
"""Every migration a schema ships, oldest first.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
MigrationFileError: A migration file does not describe a migration.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
root = release.migrations_root
|
|
70
|
+
except Exception: # noqa: BLE001 - a schema with no modules has no migrations
|
|
71
|
+
return ()
|
|
72
|
+
if not root.is_dir():
|
|
73
|
+
return ()
|
|
74
|
+
found = [load_migration(path) for path in sorted(root.glob("*.yaml"))]
|
|
75
|
+
return tuple(sorted(found, key=lambda m: m.source))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def migration_chain(
|
|
79
|
+
name: str, migrations: Sequence[Migration], start: Version, target: Version
|
|
80
|
+
) -> tuple[Migration, ...]:
|
|
81
|
+
"""The migrations that take documents written for *start* to *target*'s major.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
MigrationChainError: *start* is newer than *target*, or a major has no
|
|
85
|
+
migration out of it.
|
|
86
|
+
"""
|
|
87
|
+
if start.major > target.major:
|
|
88
|
+
raise MigrationChainError(
|
|
89
|
+
f"{name}: documents at {start} are newer than the loaded {target}"
|
|
90
|
+
)
|
|
91
|
+
chain: list[Migration] = []
|
|
92
|
+
current = start
|
|
93
|
+
while current.major < target.major:
|
|
94
|
+
out = [
|
|
95
|
+
m
|
|
96
|
+
for m in migrations
|
|
97
|
+
if m.source.major == current.major and current.major < m.target.major <= target.major
|
|
98
|
+
]
|
|
99
|
+
if not out:
|
|
100
|
+
raise MigrationChainError(
|
|
101
|
+
f"{name}: no migration from {current.major}.x towards {target} "
|
|
102
|
+
f"(the schema ships {', '.join(f'{m.from_}-to-{m.to}' for m in migrations) or 'none'})"
|
|
103
|
+
)
|
|
104
|
+
if len(out) > 1:
|
|
105
|
+
listed = ", ".join(f"{m.from_}-to-{m.to}" for m in out)
|
|
106
|
+
raise MigrationChainError(f"{name}: more than one migration from {current}: {listed}")
|
|
107
|
+
chain.append(out[0])
|
|
108
|
+
current = out[0].target
|
|
109
|
+
return tuple(chain)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _field(kind: str | None, path: str) -> str:
|
|
113
|
+
return f"{kind}.{path}" if kind else path
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def describe_step(step: Step) -> str:
|
|
117
|
+
"""One line for a step, as a dry run lists it: ``rename_field service.tags → labels``."""
|
|
118
|
+
if isinstance(step, RenameKind):
|
|
119
|
+
return f"rename_kind {step.from_} → {step.to}"
|
|
120
|
+
if isinstance(step, RenameField):
|
|
121
|
+
return f"rename_field {_field(step.kind, step.from_)} → {step.to}"
|
|
122
|
+
if isinstance(step, MoveField):
|
|
123
|
+
return f"move_field {_field(step.kind, step.from_)} → {step.to}"
|
|
124
|
+
if isinstance(step, SetDefault):
|
|
125
|
+
return f"set_default {_field(step.kind, step.field)} = {json.dumps(step.value)}"
|
|
126
|
+
if isinstance(step, DropField):
|
|
127
|
+
return f"drop_field {_field(step.kind, step.field)}"
|
|
128
|
+
if isinstance(step, DeleteKind):
|
|
129
|
+
return f"delete_kind {step.kind}"
|
|
130
|
+
if isinstance(step, MapEnum):
|
|
131
|
+
values = ", ".join(f"{k} → {v}" for k, v in step.values.items())
|
|
132
|
+
return f"map_enum {_field(step.kind, step.field)} {values}"
|
|
133
|
+
if isinstance(step, Reparent):
|
|
134
|
+
rule = (
|
|
135
|
+
f"to {step.to}"
|
|
136
|
+
if step.to is not None
|
|
137
|
+
else (f"from {step.parent_from}" if step.parent_from else f"up {step.up}")
|
|
138
|
+
)
|
|
139
|
+
return f"reparent {step.kind} {rule}"
|
|
140
|
+
if isinstance(step, MoveFiles):
|
|
141
|
+
return f"move_files {step.kind}"
|
|
142
|
+
if isinstance(step, RenameRelationship):
|
|
143
|
+
return f"rename_relationship {step.from_} → {step.to}"
|
|
144
|
+
assert isinstance(step, CodeStep)
|
|
145
|
+
return f"code {step.hook}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class SchemaChain:
|
|
150
|
+
"""One schema's migrations, from the version a backend is at to the loaded one."""
|
|
151
|
+
|
|
152
|
+
name: str
|
|
153
|
+
start: Version
|
|
154
|
+
target: Version
|
|
155
|
+
migrations: tuple[Migration, ...] = ()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class StepOutcome:
|
|
160
|
+
"""What one step did: how many documents it wrote (new or changed) and removed."""
|
|
161
|
+
|
|
162
|
+
schema: str
|
|
163
|
+
migration: Migration
|
|
164
|
+
step: Step
|
|
165
|
+
written: int
|
|
166
|
+
removed: int
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def label(self) -> str:
|
|
170
|
+
return describe_step(self.step)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class ChainPlan:
|
|
175
|
+
"""Documents after every chain, and what each step did."""
|
|
176
|
+
|
|
177
|
+
documents: DocumentSet
|
|
178
|
+
outcomes: list[StepOutcome] = field(default_factory=lambda: list[StepOutcome]())
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _key(doc: Document) -> str:
|
|
182
|
+
return json.dumps([doc.path, doc.data], sort_keys=True, default=str)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run_chains(
|
|
186
|
+
chains: Sequence[SchemaChain],
|
|
187
|
+
documents: DocumentSet,
|
|
188
|
+
*,
|
|
189
|
+
nodes: Mapping[str, NodeDefinition] | None = None,
|
|
190
|
+
hooks: Mapping[str, Callable[..., Any]] | None = None,
|
|
191
|
+
) -> ChainPlan:
|
|
192
|
+
"""Apply each chain's migrations to *documents*, a step at a time. The input is not
|
|
193
|
+
changed.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
MigrationStepError: A step cannot be applied.
|
|
197
|
+
"""
|
|
198
|
+
current = documents
|
|
199
|
+
outcomes: list[StepOutcome] = []
|
|
200
|
+
for chain in chains:
|
|
201
|
+
for migration in chain.migrations:
|
|
202
|
+
for step in migration.steps:
|
|
203
|
+
single = migration.model_copy(update={"steps": (step,)})
|
|
204
|
+
after = apply_migration(single, current, nodes=nodes, hooks=hooks)
|
|
205
|
+
before_keys = Counter(_key(d) for d in current.documents)
|
|
206
|
+
after_keys = Counter(_key(d) for d in after.documents)
|
|
207
|
+
written = sum((after_keys - before_keys).values())
|
|
208
|
+
removed = max(0, len(current.documents) - len(after.documents))
|
|
209
|
+
outcomes.append(StepOutcome(chain.name, migration, step, written, removed))
|
|
210
|
+
current = after
|
|
211
|
+
if current is documents:
|
|
212
|
+
current = copy.deepcopy(documents)
|
|
213
|
+
return ChainPlan(documents=current, outcomes=outcomes)
|