katalyst-engine 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. katalyst_engine/__init__.py +6 -0
  2. katalyst_engine/bundle/__init__.py +30 -0
  3. katalyst_engine/bundle/discovery.py +158 -0
  4. katalyst_engine/bundle/loader.py +134 -0
  5. katalyst_engine/bundle/protocol.py +209 -0
  6. katalyst_engine/core/__init__.py +62 -0
  7. katalyst_engine/core/compatibility.py +58 -0
  8. katalyst_engine/core/compositional.py +103 -0
  9. katalyst_engine/core/definitive.py +195 -0
  10. katalyst_engine/core/evolvable.py +89 -0
  11. katalyst_engine/core/identity.py +95 -0
  12. katalyst_engine/core/lifecycle.py +62 -0
  13. katalyst_engine/core/relation.py +151 -0
  14. katalyst_engine/core/version.py +203 -0
  15. katalyst_engine/discovery/__init__.py +20 -0
  16. katalyst_engine/discovery/declaration.py +74 -0
  17. katalyst_engine/discovery/dispatcher.py +83 -0
  18. katalyst_engine/discovery/protocol.py +69 -0
  19. katalyst_engine/events/__init__.py +10 -0
  20. katalyst_engine/events/bus.py +102 -0
  21. katalyst_engine/events/event.py +82 -0
  22. katalyst_engine/extensions/__init__.py +32 -0
  23. katalyst_engine/extensions/capability.py +45 -0
  24. katalyst_engine/extensions/discovery.py +85 -0
  25. katalyst_engine/extensions/effector.py +54 -0
  26. katalyst_engine/extensions/provider.py +33 -0
  27. katalyst_engine/extensions/registry.py +77 -0
  28. katalyst_engine/extensions/trigger.py +64 -0
  29. katalyst_engine/model/__init__.py +25 -0
  30. katalyst_engine/model/manager.py +85 -0
  31. katalyst_engine/model/materializer.py +78 -0
  32. katalyst_engine/model/node.py +49 -0
  33. katalyst_engine/model/query.py +186 -0
  34. katalyst_engine/model/store.py +119 -0
  35. katalyst_engine/py.typed +0 -0
  36. katalyst_engine/replication/__init__.py +30 -0
  37. katalyst_engine/replication/engine.py +104 -0
  38. katalyst_engine/replication/job.py +88 -0
  39. katalyst_engine/replication/transform.py +111 -0
  40. katalyst_engine/resolution/__init__.py +32 -0
  41. katalyst_engine/resolution/conflict.py +91 -0
  42. katalyst_engine/resolution/engine.py +131 -0
  43. katalyst_engine/resolution/strategies.py +122 -0
  44. katalyst_engine/schema/__init__.py +35 -0
  45. katalyst_engine/schema/definition.py +281 -0
  46. katalyst_engine/schema/manager.py +95 -0
  47. katalyst_engine/schema/registry.py +367 -0
  48. katalyst_engine/schema/versioning.py +115 -0
  49. katalyst_engine/snapshot/__init__.py +18 -0
  50. katalyst_engine/snapshot/diff.py +94 -0
  51. katalyst_engine/snapshot/snapshot.py +111 -0
  52. katalyst_engine/source/__init__.py +26 -0
  53. katalyst_engine/source/manifest.py +45 -0
  54. katalyst_engine/source/registry.py +122 -0
  55. katalyst_engine/source/source.py +92 -0
  56. katalyst_engine/toolkit/__init__.py +22 -0
  57. katalyst_engine/toolkit/file_ops.py +194 -0
  58. katalyst_engine/toolkit/rendering.py +58 -0
  59. katalyst_engine-2.0.0.dist-info/METADATA +50 -0
  60. katalyst_engine-2.0.0.dist-info/RECORD +61 -0
  61. katalyst_engine-2.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,45 @@
1
+ """Capabilities and extensions — what plugins can do.
2
+
3
+ A Capability describes a named ability. An Extension is the base
4
+ Evolvable for plugins, declaring what capabilities it provides.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from katalyst_engine.core.evolvable import Evolvable
10
+
11
+
12
+ class Capability(Evolvable, frozen=True):
13
+ """Describes what an extension can do.
14
+
15
+ Capabilities are named abilities that extensions declare.
16
+ The ExtensionRegistry indexes extensions by their capabilities,
17
+ enabling lookup like "find all extensions that can export data".
18
+ """
19
+
20
+ description: str = ""
21
+ """Human-readable description of this capability."""
22
+
23
+ category: str = ""
24
+ """Grouping category (e.g. "discovery", "export", "validation")."""
25
+
26
+
27
+ class Extension(Evolvable, frozen=True):
28
+ """Base Evolvable for plugins/extensions.
29
+
30
+ An Extension declares its capabilities and provides metadata
31
+ about itself. Concrete extension types (Provider, Effector)
32
+ add behavior via ABCs.
33
+ """
34
+
35
+ capabilities: tuple[str, ...] = ()
36
+ """Capability names this extension provides."""
37
+
38
+ description: str = ""
39
+ """Human-readable description of this extension."""
40
+
41
+ entry_point: str = ""
42
+ """Opaque reference to the extension's entry point (module path, URI, etc.)."""
43
+
44
+ enabled: bool = True
45
+ """Whether this extension is active."""
@@ -0,0 +1,85 @@
1
+ """Extension discovery — find extensions from entry points and directories.
2
+
3
+ Pure model for representing discovered extension locations. Actual
4
+ I/O (scanning entry points, reading directories) is done by adapters.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+
11
+ from pydantic import BaseModel
12
+
13
+
14
+ class ExtensionSourceKind(str, Enum):
15
+ """Where an extension was discovered from."""
16
+
17
+ ENTRY_POINT = "entry_point"
18
+ DIRECTORY = "directory"
19
+ INLINE = "inline"
20
+
21
+
22
+ class DiscoveredExtension(BaseModel, frozen=True):
23
+ """Metadata about a discovered (but not yet loaded) extension.
24
+
25
+ Represents the location and kind of an extension found during
26
+ scanning. Loading and instantiation happens in the adapter layer.
27
+ """
28
+
29
+ name: str
30
+ """Human-readable name of the extension."""
31
+
32
+ source_kind: ExtensionSourceKind
33
+ """How the extension was found."""
34
+
35
+ location: str
36
+ """Path, module path, or entry point string."""
37
+
38
+ group: str = ""
39
+ """Entry point group, if applicable."""
40
+
41
+ metadata: dict[str, str] = {}
42
+ """Additional metadata about the extension."""
43
+
44
+
45
+ class ExtensionDiscovery:
46
+ """Collects and deduplicates discovered extension descriptors.
47
+
48
+ This is an in-memory collector. Adapters push DiscoveredExtension
49
+ instances into it; the ExtensionRegistry uses the results to
50
+ load and register actual Extensions.
51
+
52
+ Usage:
53
+ discovery = ExtensionDiscovery()
54
+ discovery.add(DiscoveredExtension(name="neo4j-export", ...))
55
+ for ext in discovery.all():
56
+ # load and register
57
+ """
58
+
59
+ def __init__(self) -> None:
60
+ self._discovered: dict[str, DiscoveredExtension] = {} # keyed by name
61
+
62
+ def add(self, ext: DiscoveredExtension) -> None:
63
+ """Add a discovered extension. Last-write-wins on name collision."""
64
+ self._discovered[ext.name] = ext
65
+
66
+ def get(self, name: str) -> DiscoveredExtension | None:
67
+ """Look up a discovered extension by name."""
68
+ return self._discovered.get(name)
69
+
70
+ def all(self) -> list[DiscoveredExtension]:
71
+ """Return all discovered extensions, sorted by name."""
72
+ return sorted(self._discovered.values(), key=lambda e: e.name)
73
+
74
+ def by_source_kind(self, kind: ExtensionSourceKind) -> list[DiscoveredExtension]:
75
+ """Return extensions discovered from a specific source kind."""
76
+ return [e for e in self._discovered.values() if e.source_kind == kind]
77
+
78
+ @property
79
+ def count(self) -> int:
80
+ """Number of discovered extensions."""
81
+ return len(self._discovered)
82
+
83
+ def clear(self) -> None:
84
+ """Remove all discovered extensions."""
85
+ self._discovered.clear()
@@ -0,0 +1,54 @@
1
+ """Effector — abstract base for extensions that produce side effects.
2
+
3
+ Effectors are triggered by events and execute actions such as
4
+ exporting data, sending notifications, or running validations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from katalyst_engine.extensions.capability import Extension
15
+
16
+
17
+ class EffectResult(BaseModel, frozen=True):
18
+ """Result of an effector execution.
19
+
20
+ Captures whether the effect succeeded, any error message,
21
+ and optional output data.
22
+ """
23
+
24
+ success: bool = True
25
+ """Whether the effect executed successfully."""
26
+
27
+ error: str = ""
28
+ """Error message if the effect failed."""
29
+
30
+ output: dict[str, Any] = {}
31
+ """Optional output data from the effect."""
32
+
33
+
34
+ class Effector(Extension, ABC, frozen=True):
35
+ """Abstract base for extensions that produce side effects.
36
+
37
+ Effectors are triggered by events or explicit invocations.
38
+ They perform actions like exporting data, sending notifications,
39
+ or writing to external systems.
40
+
41
+ The execute method is abstract — concrete effectors must
42
+ implement it.
43
+ """
44
+
45
+ @abstractmethod
46
+ def execute(self, context: dict[str, Any]) -> EffectResult:
47
+ """Execute this effector's side effect.
48
+
49
+ Args:
50
+ context: Key-value data providing context for the effect.
51
+
52
+ Returns:
53
+ An EffectResult describing the outcome.
54
+ """
@@ -0,0 +1,33 @@
1
+ """Provider — abstract base for extensions that bring data in.
2
+
3
+ Providers are the discovery side of the extension system. They
4
+ know how to find declarations from a specific kind of source.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+
11
+ from katalyst_engine.discovery.declaration import Declaration
12
+ from katalyst_engine.extensions.capability import Extension
13
+
14
+
15
+ class Provider(Extension, ABC, frozen=True):
16
+ """Abstract base for extensions that bring data into the engine.
17
+
18
+ Providers discover declarations from external sources. Each
19
+ Provider implementation knows how to read from one kind of
20
+ source (filesystem, API, database, etc.).
21
+
22
+ The discover method is abstract — concrete providers must
23
+ implement it. The engine never does I/O directly; Providers
24
+ encapsulate all I/O.
25
+ """
26
+
27
+ @abstractmethod
28
+ def discover(self) -> list[Declaration]:
29
+ """Discover declarations from this provider's source.
30
+
31
+ Returns:
32
+ List of discovered Declarations.
33
+ """
@@ -0,0 +1,77 @@
1
+ """Extension registry — register and look up extensions by capability.
2
+
3
+ Central index for discovering extensions that provide specific
4
+ capabilities.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from katalyst_engine.extensions.capability import Extension
10
+
11
+
12
+ class ExtensionRegistry:
13
+ """Registry of extensions indexed by capability.
14
+
15
+ Provides registration, lookup by capability name, and
16
+ iteration over all registered extensions.
17
+
18
+ Usage:
19
+ registry = ExtensionRegistry()
20
+ registry.register(my_provider)
21
+ providers = registry.by_capability("discover")
22
+ """
23
+
24
+ def __init__(self) -> None:
25
+ self._extensions: dict[str, Extension] = {} # keyed by fqn
26
+ self._by_capability: dict[str, list[Extension]] = {}
27
+
28
+ def register(self, extension: Extension) -> None:
29
+ """Register an extension, indexing it by its declared capabilities."""
30
+ fqn = extension.identity.fqn
31
+ self._extensions[fqn] = extension
32
+
33
+ for cap in extension.capabilities:
34
+ if cap not in self._by_capability:
35
+ self._by_capability[cap] = []
36
+ self._by_capability[cap].append(extension)
37
+
38
+ def unregister(self, extension: Extension) -> bool:
39
+ """Remove an extension. Returns True if it existed."""
40
+ fqn = extension.identity.fqn
41
+ if fqn not in self._extensions:
42
+ return False
43
+
44
+ del self._extensions[fqn]
45
+
46
+ for cap in extension.capabilities:
47
+ cap_list = self._by_capability.get(cap)
48
+ if cap_list is not None:
49
+ cap_list[:] = [e for e in cap_list if e.identity.fqn != fqn]
50
+
51
+ return True
52
+
53
+ def get(self, fqn: str) -> Extension | None:
54
+ """Look up an extension by FQN."""
55
+ return self._extensions.get(fqn)
56
+
57
+ def by_capability(self, capability: str) -> list[Extension]:
58
+ """Return all extensions providing a given capability."""
59
+ return list(self._by_capability.get(capability, []))
60
+
61
+ def all_extensions(self) -> list[Extension]:
62
+ """Return all registered extensions, ordered by FQN."""
63
+ return sorted(self._extensions.values(), key=lambda e: e.identity.fqn)
64
+
65
+ def all_capabilities(self) -> list[str]:
66
+ """Return all known capability names, sorted."""
67
+ return sorted(self._by_capability.keys())
68
+
69
+ @property
70
+ def count(self) -> int:
71
+ """Number of registered extensions."""
72
+ return len(self._extensions)
73
+
74
+ def clear(self) -> None:
75
+ """Remove all extensions."""
76
+ self._extensions.clear()
77
+ self._by_capability.clear()
@@ -0,0 +1,64 @@
1
+ """Triggers — event patterns and conditions for effector activation.
2
+
3
+ A Trigger matches events by type pattern and optional conditions.
4
+ TriggerContext carries the data passed to effectors when triggered.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from katalyst_engine.events.event import Event
14
+
15
+
16
+ class Trigger(BaseModel, frozen=True):
17
+ """Describes when an effector should fire.
18
+
19
+ Matches events by type pattern (glob) and optional conditions
20
+ on event properties.
21
+ """
22
+
23
+ event_type_pattern: str
24
+ """Glob pattern matching event type strings (e.g. "model.node.*")."""
25
+
26
+ conditions: dict[str, str] = {}
27
+ """Key-value conditions on event properties. All must match."""
28
+
29
+ description: str = ""
30
+ """Human-readable description of when this trigger fires."""
31
+
32
+ def matches(self, event: Event) -> bool:
33
+ """Check if an event matches this trigger's pattern and conditions.
34
+
35
+ Uses fnmatch for the event type pattern and exact matching
36
+ for property conditions.
37
+ """
38
+ from fnmatch import fnmatch
39
+
40
+ if not fnmatch(event.type.value, self.event_type_pattern):
41
+ return False
42
+
43
+ for key, value in self.conditions.items():
44
+ if str(event.properties.get(key, "")) != value:
45
+ return False
46
+
47
+ return True
48
+
49
+
50
+ class TriggerContext(BaseModel, frozen=True):
51
+ """Data passed to effectors when a trigger fires.
52
+
53
+ Carries the triggering event plus any additional contextual
54
+ data the engine wants to provide.
55
+ """
56
+
57
+ event: Event
58
+ """The event that caused the trigger to fire."""
59
+
60
+ trigger: Trigger
61
+ """The trigger that matched."""
62
+
63
+ extra: dict[str, Any] = {}
64
+ """Additional context data."""
@@ -0,0 +1,25 @@
1
+ """Model — nodes, store, materialization, and queries."""
2
+
3
+ from katalyst_engine.model.manager import ModelManager
4
+ from katalyst_engine.model.materializer import RelationshipMaterializer
5
+ from katalyst_engine.model.node import Node
6
+ from katalyst_engine.model.query import (
7
+ QueryEngine,
8
+ QueryFilter,
9
+ QueryResult,
10
+ SortField,
11
+ SortOrder,
12
+ )
13
+ from katalyst_engine.model.store import ModelStore
14
+
15
+ __all__ = [
16
+ "ModelManager",
17
+ "ModelStore",
18
+ "Node",
19
+ "QueryEngine",
20
+ "QueryFilter",
21
+ "QueryResult",
22
+ "RelationshipMaterializer",
23
+ "SortField",
24
+ "SortOrder",
25
+ ]
@@ -0,0 +1,85 @@
1
+ """Model manager — orchestrates store, materializer, and queries.
2
+
3
+ High-level interface for adding nodes, materializing relationships,
4
+ and querying the model.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from katalyst_engine.core.relation import Relation, RelationshipClaim
10
+ from katalyst_engine.model.materializer import RelationshipMaterializer
11
+ from katalyst_engine.model.node import Node
12
+ from katalyst_engine.model.query import QueryEngine, QueryFilter, QueryResult, SortField, SortOrder
13
+ from katalyst_engine.model.store import ModelStore
14
+
15
+
16
+ class ModelManager:
17
+ """Orchestrator for the model layer.
18
+
19
+ Coordinates the ModelStore (storage), RelationshipMaterializer
20
+ (edge creation), and QueryEngine (retrieval).
21
+
22
+ Usage:
23
+ manager = ModelManager()
24
+ manager.add_node(my_node)
25
+ relations = manager.materialize(claims)
26
+ result = manager.query(QueryFilter(kind="layer"))
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ self._store = ModelStore()
31
+ self._materializer = RelationshipMaterializer()
32
+ self._query_engine = QueryEngine(self._store)
33
+ self._relations: list[Relation] = []
34
+
35
+ @property
36
+ def store(self) -> ModelStore:
37
+ """Access the underlying ModelStore."""
38
+ return self._store
39
+
40
+ @property
41
+ def relations(self) -> list[Relation]:
42
+ """All materialized relations."""
43
+ return list(self._relations)
44
+
45
+ def add_node(self, node: Node) -> None:
46
+ """Add a node to the model."""
47
+ self._store.add(node)
48
+
49
+ def remove_node(self, node: Node) -> bool:
50
+ """Remove a node from the model. Returns True if it existed."""
51
+ return self._store.remove(node.identity)
52
+
53
+ def materialize(self, claims: list[RelationshipClaim]) -> list[Relation]:
54
+ """Materialize relationship claims against the current store.
55
+
56
+ Replaces any previously materialized relations.
57
+
58
+ Returns:
59
+ The list of materialized Relations.
60
+ """
61
+ all_nodes = self._store.all_nodes()
62
+ self._relations = self._materializer.materialize(claims, all_nodes)
63
+ return list(self._relations)
64
+
65
+ def query(
66
+ self,
67
+ filter: QueryFilter | None = None,
68
+ sort_by: SortField = SortField.FQN,
69
+ sort_order: SortOrder = SortOrder.ASC,
70
+ offset: int = 0,
71
+ limit: int | None = None,
72
+ ) -> QueryResult:
73
+ """Query nodes in the model."""
74
+ return self._query_engine.query(
75
+ filter=filter,
76
+ sort_by=sort_by,
77
+ sort_order=sort_order,
78
+ offset=offset,
79
+ limit=limit,
80
+ )
81
+
82
+ def clear(self) -> None:
83
+ """Remove all nodes and relations."""
84
+ self._store.clear()
85
+ self._relations.clear()
@@ -0,0 +1,78 @@
1
+ """Relationship materializer — turns claims into concrete Relations.
2
+
3
+ Takes Nodes and their RelationshipClaims, matches targets via
4
+ NodeSelectors, and produces materialized Relation edges.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from katalyst_engine.core.identity import Identity
10
+ from katalyst_engine.core.relation import Relation, RelationshipClaim
11
+ from katalyst_engine.core.version import Version
12
+ from katalyst_engine.model.node import Node
13
+
14
+
15
+ class RelationshipMaterializer:
16
+ """Materializes RelationshipClaims into concrete Relations.
17
+
18
+ Given a set of nodes and their claims, finds matching targets
19
+ using NodeSelectors and produces Relation edges.
20
+
21
+ Usage:
22
+ materializer = RelationshipMaterializer()
23
+ relations = materializer.materialize(claims, nodes)
24
+ """
25
+
26
+ def materialize(
27
+ self,
28
+ claims: list[RelationshipClaim],
29
+ nodes: list[Node],
30
+ ) -> list[Relation]:
31
+ """Materialize claims against a set of available nodes.
32
+
33
+ For each claim, evaluates the selector against all nodes
34
+ (excluding the source node) and produces a Relation for
35
+ each match.
36
+
37
+ Args:
38
+ claims: Relationship claims to materialize.
39
+ nodes: Available nodes to match against.
40
+
41
+ Returns:
42
+ List of materialized Relations.
43
+ """
44
+ relations: list[Relation] = []
45
+
46
+ for claim in claims:
47
+ matches = self._find_matches(claim, nodes)
48
+ for target in matches:
49
+ relation = self._build_relation(claim, target)
50
+ relations.append(relation)
51
+
52
+ return relations
53
+
54
+ def _find_matches(self, claim: RelationshipClaim, nodes: list[Node]) -> list[Node]:
55
+ """Find nodes matching a claim's selector, excluding the source."""
56
+ return [
57
+ node
58
+ for node in nodes
59
+ if node.identity != claim.source_identity and claim.selector.matches(node)
60
+ ]
61
+
62
+ def _build_relation(self, claim: RelationshipClaim, target: Node) -> Relation:
63
+ """Build a Relation from a claim and a matched target."""
64
+ rel_identity = Identity(
65
+ kind="relation",
66
+ name=f"{claim.source_identity.fqn}--{claim.kind}-->{target.identity.fqn}",
67
+ namespace=claim.source_identity.namespace,
68
+ )
69
+
70
+ return Relation(
71
+ identity=rel_identity,
72
+ version=Version(),
73
+ source=claim.source_identity,
74
+ target=target.identity,
75
+ relation_kind=claim.kind,
76
+ cardinality=claim.cardinality,
77
+ materialized_from=claim,
78
+ )
@@ -0,0 +1,49 @@
1
+ """Node — concrete Evolvable instances with spec data and parent references.
2
+
3
+ A Node is the runtime representation of a declared artifact. It carries
4
+ a spec dict (arbitrary key-value data), a source FQN indicating where
5
+ it was discovered, and an optional parent identity reference.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from katalyst_engine.core.evolvable import Evolvable
13
+ from katalyst_engine.core.identity import Identity
14
+
15
+
16
+ class Node(Evolvable, frozen=True):
17
+ """A concrete artifact instance in the model.
18
+
19
+ Nodes are the primary inhabitants of the ModelStore. Each Node
20
+ has an identity (kind, name, namespace), a spec dict carrying
21
+ its domain-specific data, and references to its source and parent.
22
+
23
+ Nodes are immutable — updates produce new Node instances.
24
+ """
25
+
26
+ spec: dict[str, Any] = {}
27
+ """Domain-specific data for this node (field values from the declaration)."""
28
+
29
+ source_fqn: str = ""
30
+ """FQN of the source that produced this node."""
31
+
32
+ parent: Identity | None = None
33
+ """Identity of this node's parent, if any."""
34
+
35
+ description: str = ""
36
+ """Human-readable description of this node."""
37
+
38
+ def with_spec(self, **updates: Any) -> Node:
39
+ """Return a new Node with updated spec fields."""
40
+ merged: dict[str, Any] = {**self.spec, **updates}
41
+ return self.model_copy(update={"spec": merged})
42
+
43
+ def with_parent(self, parent: Identity) -> Node:
44
+ """Return a new Node with the given parent identity."""
45
+ return self.model_copy(update={"parent": parent})
46
+
47
+ def spec_value(self, key: str, default: Any = None) -> Any:
48
+ """Get a value from the spec dict with an optional default."""
49
+ return self.spec.get(key, default)