katalyst-engine 5.0.1a2__tar.gz → 5.0.1a3__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.1a3}/PKG-INFO +2 -2
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/pyproject.toml +2 -2
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/bundle/loader.py +14 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/evolution/__init__.py +37 -0
- katalyst_engine-5.0.1a3/src/katalyst_engine/evolution/chain.py +213 -0
- katalyst_engine-5.0.1a3/src/katalyst_engine/evolution/lock.py +228 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/toolkit/linkml.py +72 -2
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/evolution/conftest.py +0 -7
- katalyst_engine-5.0.1a3/tests/evolution/test_chain.py +99 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/evolution/test_diff.py +2 -2
- katalyst_engine-5.0.1a3/tests/evolution/test_lock.py +94 -0
- katalyst_engine-5.0.1a3/tests/toolkit/test_core_parent.py +171 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/.gitignore +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/CHANGELOG.md +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/README.md +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/bundle/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/bundle/discovery.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/bundle/path_loader.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/bundle/policy.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/bundle/protocol.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/compatibility.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/compositional.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/definitive.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/errors.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/evolvable.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/identity.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/lifecycle.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/matching.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/relation.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/core/version.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/discovery/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/discovery/declaration.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/discovery/dispatcher.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/discovery/protocol.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/events/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/events/bus.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/events/event.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/evolution/diff.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/evolution/gate.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/evolution/migration.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/evolution/release.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/extensions/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/extensions/capability.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/extensions/discovery.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/extensions/effector.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/extensions/provider.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/extensions/registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/extensions/trigger.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/model/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/model/manager.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/model/materializer.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/model/node.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/model/query.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/model/store.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/py.typed +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/replication/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/replication/engine.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/replication/job.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/replication/transform.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/resolution/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/resolution/conflict.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/resolution/engine.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/resolution/strategies.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/schema/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/schema/conflict.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/schema/definition.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/schema/manager.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/schema/registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/schema/versioning.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/snapshot/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/snapshot/diff.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/snapshot/snapshot.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/source/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/source/manifest.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/source/registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/source/source.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/toolkit/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/toolkit/file_ops.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/toolkit/rendering.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/bundle/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/bundle/test_discovery.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/bundle/test_loader.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/bundle/test_loader_policy.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/bundle/test_path_loader.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/bundle/test_protocol.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/conftest.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_compatibility.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_compositional.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_definitive.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_evolvable.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_identity.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_init.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_lifecycle.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_matching.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_matching_consolidation.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_relation.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_relation_selectors.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/core/test_version.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/discovery/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/discovery/test_declaration.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/discovery/test_dispatcher.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/events/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/events/test_bus.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/evolution/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/evolution/test_gate.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/evolution/test_migration.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/evolution/test_release.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/extensions/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/extensions/test_effector.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/extensions/test_provider.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/extensions/test_registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/model/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/model/test_materializer.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/model/test_node.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/model/test_query.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/model/test_store.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/replication/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/replication/test_engine.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/resolution/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/resolution/test_engine.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/resolution/test_strategies.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_definition.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_engine_boundary_guard.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_field_role.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_fieldspec_allowed_values.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_manager.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_plugin_settings.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_registry.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_registry_provenance.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_storage_policy.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/schema/test_versioning.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/snapshot/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/snapshot/test_diff.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/snapshot/test_snapshot.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/source/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/source/test_source.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/test_version.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/toolkit/__init__.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/toolkit/test_file_ops.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/tests/toolkit/test_linkml_bundle.py +0 -0
- {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/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.1a3
|
|
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.1a3"
|
|
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
|
|
|
@@ -107,6 +107,7 @@ class BundleLoader:
|
|
|
107
107
|
manifest = manifest.model_copy(update={"entry_point": entry_point})
|
|
108
108
|
|
|
109
109
|
provider = name or discovered_name or fqn
|
|
110
|
+
self._register_core_relationships()
|
|
110
111
|
with self.manager.registry.providing(provider):
|
|
111
112
|
bundle.register(self.manager)
|
|
112
113
|
|
|
@@ -122,6 +123,19 @@ class BundleLoader:
|
|
|
122
123
|
|
|
123
124
|
return manifest
|
|
124
125
|
|
|
126
|
+
def _register_core_relationships(self) -> None:
|
|
127
|
+
"""katalyst-core's relationship types (``child_of``), once and unattributed.
|
|
128
|
+
|
|
129
|
+
Every bundle uses them and none defines them, so they belong to no provider and
|
|
130
|
+
never conflict; a view narrowed to some bundles keeps them.
|
|
131
|
+
"""
|
|
132
|
+
from katalyst_engine.toolkit.linkml import core_relationships
|
|
133
|
+
|
|
134
|
+
registry = self.manager.registry
|
|
135
|
+
for rel_def in core_relationships():
|
|
136
|
+
if registry.get_relationship(rel_def.describes_kind) is None:
|
|
137
|
+
registry.register_relationship(rel_def)
|
|
138
|
+
|
|
125
139
|
def discover_and_register(
|
|
126
140
|
self,
|
|
127
141
|
discovery: BundleDiscovery | None = None,
|
{katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a3}/src/katalyst_engine/evolution/__init__.py
RENAMED
|
@@ -7,8 +7,21 @@ 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:`.chain`: the chain of migrations from a recorded version to the loaded one, run
|
|
12
|
+
a step at a time.
|
|
10
13
|
"""
|
|
11
14
|
|
|
15
|
+
from katalyst_engine.evolution.chain import (
|
|
16
|
+
ChainPlan,
|
|
17
|
+
MigrationChainError,
|
|
18
|
+
SchemaChain,
|
|
19
|
+
StepOutcome,
|
|
20
|
+
describe_step,
|
|
21
|
+
migration_chain,
|
|
22
|
+
release_migrations,
|
|
23
|
+
run_chains,
|
|
24
|
+
)
|
|
12
25
|
from katalyst_engine.evolution.diff import (
|
|
13
26
|
Category,
|
|
14
27
|
Impact,
|
|
@@ -18,6 +31,15 @@ from katalyst_engine.evolution.diff import (
|
|
|
18
31
|
diff_modules,
|
|
19
32
|
)
|
|
20
33
|
from katalyst_engine.evolution.gate import GateReport, check_release, migration_skeleton
|
|
34
|
+
from katalyst_engine.evolution.lock import (
|
|
35
|
+
LOCK_FILE_NAME,
|
|
36
|
+
BundleStatus,
|
|
37
|
+
SchemaLock,
|
|
38
|
+
SchemaLockError,
|
|
39
|
+
SchemaState,
|
|
40
|
+
SchemaStatus,
|
|
41
|
+
compare_lock,
|
|
42
|
+
)
|
|
21
43
|
from katalyst_engine.evolution.migration import (
|
|
22
44
|
CODE_HOOK_GROUP,
|
|
23
45
|
Document,
|
|
@@ -38,25 +60,40 @@ from katalyst_engine.evolution.release import (
|
|
|
38
60
|
|
|
39
61
|
__all__ = [
|
|
40
62
|
"CODE_HOOK_GROUP",
|
|
63
|
+
"LOCK_FILE_NAME",
|
|
64
|
+
"BundleStatus",
|
|
41
65
|
"Category",
|
|
66
|
+
"ChainPlan",
|
|
42
67
|
"Document",
|
|
43
68
|
"DocumentSet",
|
|
44
69
|
"GateReport",
|
|
45
70
|
"Impact",
|
|
46
71
|
"Location",
|
|
47
72
|
"Migration",
|
|
73
|
+
"MigrationChainError",
|
|
48
74
|
"MigrationFileError",
|
|
49
75
|
"MigrationStepError",
|
|
76
|
+
"SchemaChain",
|
|
50
77
|
"SchemaChange",
|
|
51
78
|
"SchemaDiff",
|
|
79
|
+
"SchemaLock",
|
|
80
|
+
"SchemaLockError",
|
|
52
81
|
"SchemaRelease",
|
|
82
|
+
"SchemaState",
|
|
83
|
+
"SchemaStatus",
|
|
53
84
|
"SchemaVersionMismatchError",
|
|
85
|
+
"StepOutcome",
|
|
54
86
|
"apply_migration",
|
|
55
87
|
"bundle_release",
|
|
56
88
|
"check_release",
|
|
89
|
+
"compare_lock",
|
|
57
90
|
"core_release",
|
|
91
|
+
"describe_step",
|
|
58
92
|
"diff_modules",
|
|
59
93
|
"installed_releases",
|
|
60
94
|
"load_migration",
|
|
95
|
+
"migration_chain",
|
|
61
96
|
"migration_skeleton",
|
|
97
|
+
"release_migrations",
|
|
98
|
+
"run_chains",
|
|
62
99
|
]
|
|
@@ -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)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""The schema lock: the version of each schema a backend's documents were written with.
|
|
2
|
+
|
|
3
|
+
A backend records it beside its documents (a repository in a file, a database in a
|
|
4
|
+
metadata table), and compares it with the schemas it is opened with:
|
|
5
|
+
|
|
6
|
+
- **current**: every recorded major is the loaded major (a minor may differ);
|
|
7
|
+
- **behind**: a recorded major is older than the loaded one; the backend can be read,
|
|
8
|
+
not written, until its documents are migrated;
|
|
9
|
+
- **ahead**: a recorded major is newer than the loaded one; the backend was written by
|
|
10
|
+
a newer release and is not read at all;
|
|
11
|
+
- **untracked**: the backend cannot record versions.
|
|
12
|
+
|
|
13
|
+
A backend with no lock was written before versions were recorded. It is taken to be at
|
|
14
|
+
each schema's *baseline*, the oldest archived release (nothing older is supported), or
|
|
15
|
+
at the loaded version for a schema with no archive. A schema the lock does not name was
|
|
16
|
+
added after it was written, and is current; one the lock names that is not loaded is
|
|
17
|
+
reported, and decides nothing.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Mapping
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from enum import StrEnum
|
|
25
|
+
from typing import Any, cast
|
|
26
|
+
|
|
27
|
+
import yaml
|
|
28
|
+
|
|
29
|
+
from katalyst_engine.core.version import Version
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"LOCK_FILE_NAME",
|
|
33
|
+
"BundleStatus",
|
|
34
|
+
"SchemaLock",
|
|
35
|
+
"SchemaLockError",
|
|
36
|
+
"SchemaState",
|
|
37
|
+
"SchemaStatus",
|
|
38
|
+
"compare_lock",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
LOCK_FILE_NAME = "schema.lock"
|
|
42
|
+
"""The file a repository keeps its lock in (beside its documents)."""
|
|
43
|
+
|
|
44
|
+
_HEADER = "# Written by kata. Change it with `kata tax migrate`, not by hand.\n"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SchemaLockError(ValueError):
|
|
48
|
+
"""A lock that cannot be read."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class SchemaLock:
|
|
53
|
+
"""The version of each schema, by schema name."""
|
|
54
|
+
|
|
55
|
+
bundles: Mapping[str, Version] = field(default_factory=lambda: dict[str, Version]())
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def parse(cls, text: str) -> SchemaLock:
|
|
59
|
+
"""Read a lock file's text (``bundles: {name: version}``).
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
SchemaLockError: It is not a lock.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
raw: Any = yaml.safe_load(text)
|
|
66
|
+
except yaml.YAMLError as exc:
|
|
67
|
+
raise SchemaLockError(f"not YAML: {exc}") from exc
|
|
68
|
+
if not isinstance(raw, dict) or not isinstance(
|
|
69
|
+
cast("dict[str, Any]", raw).get("bundles"), dict
|
|
70
|
+
):
|
|
71
|
+
raise SchemaLockError("a schema lock is a mapping with `bundles: {name: version}`")
|
|
72
|
+
bundles: dict[str, Version] = {}
|
|
73
|
+
for name, version in cast("dict[str, Any]", cast("dict[str, Any]", raw)["bundles"]).items():
|
|
74
|
+
try:
|
|
75
|
+
bundles[str(name)] = Version.parse(str(version))
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
raise SchemaLockError(f"{name}: {version!r} is not a version") from exc
|
|
78
|
+
return cls(bundles=bundles)
|
|
79
|
+
|
|
80
|
+
def render(self) -> str:
|
|
81
|
+
"""The lock file's text: the schemas in name order."""
|
|
82
|
+
body = yaml.safe_dump(
|
|
83
|
+
{"bundles": {name: str(self.bundles[name]) for name in sorted(self.bundles)}},
|
|
84
|
+
sort_keys=False,
|
|
85
|
+
default_flow_style=False,
|
|
86
|
+
)
|
|
87
|
+
return _HEADER + body
|
|
88
|
+
|
|
89
|
+
def with_versions(self, versions: Mapping[str, Version]) -> SchemaLock:
|
|
90
|
+
"""This lock with *versions* recorded (the schemas it names and *versions* does
|
|
91
|
+
not are kept)."""
|
|
92
|
+
return SchemaLock(bundles={**self.bundles, **versions})
|
|
93
|
+
|
|
94
|
+
def as_strings(self) -> dict[str, str]:
|
|
95
|
+
return {name: str(self.bundles[name]) for name in sorted(self.bundles)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SchemaState(StrEnum):
|
|
99
|
+
"""How a backend's recorded versions compare with the loaded schemas."""
|
|
100
|
+
|
|
101
|
+
CURRENT = "current"
|
|
102
|
+
BEHIND = "behind"
|
|
103
|
+
AHEAD = "ahead"
|
|
104
|
+
UNTRACKED = "untracked"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class BundleStatus:
|
|
109
|
+
"""One schema: what is loaded, what the backend is at, and how they compare."""
|
|
110
|
+
|
|
111
|
+
name: str
|
|
112
|
+
loaded: Version | None
|
|
113
|
+
"""The loaded version; ``None``: the lock names a schema that is not loaded."""
|
|
114
|
+
|
|
115
|
+
recorded: Version | None
|
|
116
|
+
"""What the backend is at: recorded, or (:attr:`assumed`) taken to be."""
|
|
117
|
+
|
|
118
|
+
assumed: bool = False
|
|
119
|
+
"""The lock does not say: :attr:`recorded` is the baseline (no lock), or the loaded
|
|
120
|
+
version (a schema added after the lock was written)."""
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def state(self) -> SchemaState | None:
|
|
124
|
+
"""``None`` for a schema that is not loaded."""
|
|
125
|
+
if self.loaded is None:
|
|
126
|
+
return None
|
|
127
|
+
if self.recorded is None or self.recorded.major == self.loaded.major:
|
|
128
|
+
return SchemaState.CURRENT
|
|
129
|
+
return SchemaState.BEHIND if self.recorded.major < self.loaded.major else SchemaState.AHEAD
|
|
130
|
+
|
|
131
|
+
def to_dict(self) -> dict[str, Any]:
|
|
132
|
+
return {
|
|
133
|
+
"name": self.name,
|
|
134
|
+
"loaded": str(self.loaded) if self.loaded is not None else None,
|
|
135
|
+
"recorded": str(self.recorded) if self.recorded is not None else None,
|
|
136
|
+
"assumed": self.assumed,
|
|
137
|
+
"state": self.state.value if self.state is not None else "not-loaded",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class SchemaStatus:
|
|
143
|
+
"""A backend's schema versions against the loaded schemas."""
|
|
144
|
+
|
|
145
|
+
backend: str
|
|
146
|
+
state: SchemaState
|
|
147
|
+
recorded: bool
|
|
148
|
+
"""Whether the backend has a lock (``False``: unrecorded, or untracked)."""
|
|
149
|
+
|
|
150
|
+
bundles: tuple[BundleStatus, ...] = ()
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def behind(self) -> tuple[BundleStatus, ...]:
|
|
154
|
+
return tuple(b for b in self.bundles if b.state is SchemaState.BEHIND)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def ahead(self) -> tuple[BundleStatus, ...]:
|
|
158
|
+
return tuple(b for b in self.bundles if b.state is SchemaState.AHEAD)
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def writable(self) -> bool:
|
|
162
|
+
"""Whether the schema versions allow a write (current or untracked)."""
|
|
163
|
+
return self.state in (SchemaState.CURRENT, SchemaState.UNTRACKED)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def readable(self) -> bool:
|
|
167
|
+
return self.state is not SchemaState.AHEAD
|
|
168
|
+
|
|
169
|
+
def recorded_versions(self) -> dict[str, Version]:
|
|
170
|
+
"""What the backend is at (recorded or assumed), for every loaded schema."""
|
|
171
|
+
return {
|
|
172
|
+
b.name: b.recorded
|
|
173
|
+
for b in self.bundles
|
|
174
|
+
if b.loaded is not None and b.recorded is not None
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict[str, Any]:
|
|
178
|
+
return {
|
|
179
|
+
"backend": self.backend,
|
|
180
|
+
"state": self.state.value,
|
|
181
|
+
"recorded": self.recorded,
|
|
182
|
+
"writable": self.writable,
|
|
183
|
+
"bundles": [b.to_dict() for b in self.bundles],
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def compare_lock(
|
|
188
|
+
backend: str,
|
|
189
|
+
lock: SchemaLock | None,
|
|
190
|
+
loaded: Mapping[str, Version],
|
|
191
|
+
*,
|
|
192
|
+
baselines: Mapping[str, Version] | None = None,
|
|
193
|
+
tracked: bool = True,
|
|
194
|
+
) -> SchemaStatus:
|
|
195
|
+
"""*backend*'s :class:`SchemaStatus`.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
backend: The backend's name.
|
|
199
|
+
lock: What it records; ``None`` when it records nothing.
|
|
200
|
+
loaded: The loaded schemas' versions, by name.
|
|
201
|
+
baselines: Each schema's oldest archived release, what a backend with no lock is
|
|
202
|
+
taken to be at. A schema without one: its loaded version.
|
|
203
|
+
tracked: Whether the backend can record versions at all.
|
|
204
|
+
"""
|
|
205
|
+
if not tracked:
|
|
206
|
+
rows = tuple(
|
|
207
|
+
BundleStatus(name, loaded[name], None, assumed=True) for name in sorted(loaded)
|
|
208
|
+
)
|
|
209
|
+
return SchemaStatus(backend, SchemaState.UNTRACKED, recorded=False, bundles=rows)
|
|
210
|
+
bases = baselines or {}
|
|
211
|
+
rows_list: list[BundleStatus] = []
|
|
212
|
+
for name in sorted(set(loaded) | set(lock.bundles if lock else ())):
|
|
213
|
+
current = loaded.get(name)
|
|
214
|
+
if lock is None:
|
|
215
|
+
rows_list.append(BundleStatus(name, current, bases.get(name, current), assumed=True))
|
|
216
|
+
elif name in lock.bundles:
|
|
217
|
+
rows_list.append(BundleStatus(name, current, lock.bundles[name]))
|
|
218
|
+
else:
|
|
219
|
+
rows_list.append(BundleStatus(name, current, current, assumed=True))
|
|
220
|
+
rows = tuple(rows_list)
|
|
221
|
+
states = {row.state for row in rows}
|
|
222
|
+
if SchemaState.AHEAD in states:
|
|
223
|
+
state = SchemaState.AHEAD
|
|
224
|
+
elif SchemaState.BEHIND in states:
|
|
225
|
+
state = SchemaState.BEHIND
|
|
226
|
+
else:
|
|
227
|
+
state = SchemaState.CURRENT
|
|
228
|
+
return SchemaStatus(backend, state, recorded=lock is not None, bundles=rows)
|