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.
- katalyst_engine/__init__.py +6 -0
- katalyst_engine/bundle/__init__.py +30 -0
- katalyst_engine/bundle/discovery.py +158 -0
- katalyst_engine/bundle/loader.py +134 -0
- katalyst_engine/bundle/protocol.py +209 -0
- katalyst_engine/core/__init__.py +62 -0
- katalyst_engine/core/compatibility.py +58 -0
- katalyst_engine/core/compositional.py +103 -0
- katalyst_engine/core/definitive.py +195 -0
- katalyst_engine/core/evolvable.py +89 -0
- katalyst_engine/core/identity.py +95 -0
- katalyst_engine/core/lifecycle.py +62 -0
- katalyst_engine/core/relation.py +151 -0
- katalyst_engine/core/version.py +203 -0
- katalyst_engine/discovery/__init__.py +20 -0
- katalyst_engine/discovery/declaration.py +74 -0
- katalyst_engine/discovery/dispatcher.py +83 -0
- katalyst_engine/discovery/protocol.py +69 -0
- katalyst_engine/events/__init__.py +10 -0
- katalyst_engine/events/bus.py +102 -0
- katalyst_engine/events/event.py +82 -0
- katalyst_engine/extensions/__init__.py +32 -0
- katalyst_engine/extensions/capability.py +45 -0
- katalyst_engine/extensions/discovery.py +85 -0
- katalyst_engine/extensions/effector.py +54 -0
- katalyst_engine/extensions/provider.py +33 -0
- katalyst_engine/extensions/registry.py +77 -0
- katalyst_engine/extensions/trigger.py +64 -0
- katalyst_engine/model/__init__.py +25 -0
- katalyst_engine/model/manager.py +85 -0
- katalyst_engine/model/materializer.py +78 -0
- katalyst_engine/model/node.py +49 -0
- katalyst_engine/model/query.py +186 -0
- katalyst_engine/model/store.py +119 -0
- katalyst_engine/py.typed +0 -0
- katalyst_engine/replication/__init__.py +30 -0
- katalyst_engine/replication/engine.py +104 -0
- katalyst_engine/replication/job.py +88 -0
- katalyst_engine/replication/transform.py +111 -0
- katalyst_engine/resolution/__init__.py +32 -0
- katalyst_engine/resolution/conflict.py +91 -0
- katalyst_engine/resolution/engine.py +131 -0
- katalyst_engine/resolution/strategies.py +122 -0
- katalyst_engine/schema/__init__.py +35 -0
- katalyst_engine/schema/definition.py +281 -0
- katalyst_engine/schema/manager.py +95 -0
- katalyst_engine/schema/registry.py +367 -0
- katalyst_engine/schema/versioning.py +115 -0
- katalyst_engine/snapshot/__init__.py +18 -0
- katalyst_engine/snapshot/diff.py +94 -0
- katalyst_engine/snapshot/snapshot.py +111 -0
- katalyst_engine/source/__init__.py +26 -0
- katalyst_engine/source/manifest.py +45 -0
- katalyst_engine/source/registry.py +122 -0
- katalyst_engine/source/source.py +92 -0
- katalyst_engine/toolkit/__init__.py +22 -0
- katalyst_engine/toolkit/file_ops.py +194 -0
- katalyst_engine/toolkit/rendering.py +58 -0
- katalyst_engine-2.0.0.dist-info/METADATA +50 -0
- katalyst_engine-2.0.0.dist-info/RECORD +61 -0
- 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)
|