constellation-node-sdk 1.0.1__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 (45) hide show
  1. constellation_node_sdk/__init__.py +123 -0
  2. constellation_node_sdk/gate/__init__.py +47 -0
  3. constellation_node_sdk/gate/client.py +108 -0
  4. constellation_node_sdk/gate/config.py +153 -0
  5. constellation_node_sdk/gate/errors.py +17 -0
  6. constellation_node_sdk/gate/policy.py +70 -0
  7. constellation_node_sdk/gate/registration.py +144 -0
  8. constellation_node_sdk/orchestrator/__init__.py +3 -0
  9. constellation_node_sdk/orchestrator/base.py +57 -0
  10. constellation_node_sdk/orchestrator/merge.py +47 -0
  11. constellation_node_sdk/orchestrator/packet_builder.py +49 -0
  12. constellation_node_sdk/orchestrator/retry.py +61 -0
  13. constellation_node_sdk/orchestrator/state.py +78 -0
  14. constellation_node_sdk/orchestrator/step_executor.py +87 -0
  15. constellation_node_sdk/py.typed +0 -0
  16. constellation_node_sdk/runtime/__init__.py +52 -0
  17. constellation_node_sdk/runtime/app.py +274 -0
  18. constellation_node_sdk/runtime/config.py +307 -0
  19. constellation_node_sdk/runtime/errors.py +112 -0
  20. constellation_node_sdk/runtime/execution.py +249 -0
  21. constellation_node_sdk/runtime/handlers.py +45 -0
  22. constellation_node_sdk/runtime/inbound_policy.py +86 -0
  23. constellation_node_sdk/runtime/lifecycle.py +29 -0
  24. constellation_node_sdk/runtime/observability.py +72 -0
  25. constellation_node_sdk/runtime/preflight.py +122 -0
  26. constellation_node_sdk/security/__init__.py +36 -0
  27. constellation_node_sdk/security/delegation.py +57 -0
  28. constellation_node_sdk/security/errors.py +25 -0
  29. constellation_node_sdk/security/signing.py +121 -0
  30. constellation_node_sdk/security/validation.py +230 -0
  31. constellation_node_sdk/security/verification.py +107 -0
  32. constellation_node_sdk/transport/__init__.py +88 -0
  33. constellation_node_sdk/transport/codec.py +21 -0
  34. constellation_node_sdk/transport/errors.py +41 -0
  35. constellation_node_sdk/transport/hashing.py +73 -0
  36. constellation_node_sdk/transport/hop_trace.py +378 -0
  37. constellation_node_sdk/transport/lineage.py +28 -0
  38. constellation_node_sdk/transport/models.py +447 -0
  39. constellation_node_sdk/transport/packet.py +331 -0
  40. constellation_node_sdk/transport/provenance.py +53 -0
  41. constellation_node_sdk/transport/tenant.py +76 -0
  42. constellation_node_sdk-1.0.1.dist-info/METADATA +141 -0
  43. constellation_node_sdk-1.0.1.dist-info/RECORD +45 -0
  44. constellation_node_sdk-1.0.1.dist-info/WHEEL +5 -0
  45. constellation_node_sdk-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from constellation_node_sdk.gate.client import GateClient
6
+ from constellation_node_sdk.transport.packet import TransportPacket
7
+
8
+ from .state import OrchestratorState
9
+ from .step_executor import StepExecutor
10
+
11
+
12
+ class BaseOrchestrator(ABC):
13
+ """
14
+ Base class for packet-native orchestrators.
15
+
16
+ Orchestrators are stateful internal clients of Gate. They never resolve
17
+ peer nodes directly and never send step packets to peer URLs.
18
+ """
19
+
20
+ def __init__(self, *, gate_client: GateClient, source_node: str) -> None:
21
+ normalized_source = source_node.strip().lower()
22
+ if not normalized_source:
23
+ raise ValueError("source_node must not be empty")
24
+
25
+ self._gate_client = gate_client
26
+ self._source_node = normalized_source
27
+ self._step_executor = StepExecutor(
28
+ gate_client=gate_client,
29
+ source_node=normalized_source,
30
+ )
31
+
32
+ @property
33
+ def gate_client(self) -> GateClient:
34
+ return self._gate_client
35
+
36
+ @property
37
+ def source_node(self) -> str:
38
+ return self._source_node
39
+
40
+ @property
41
+ def step_executor(self) -> StepExecutor:
42
+ return self._step_executor
43
+
44
+ def initial_state(self, packet: TransportPacket) -> OrchestratorState:
45
+ """
46
+ Build the default initial workflow state from the inbound packet.
47
+ """
48
+ return OrchestratorState.from_packet(
49
+ packet=packet,
50
+ orchestrator_node=self._source_node,
51
+ )
52
+
53
+ @abstractmethod
54
+ async def execute(self, packet: TransportPacket) -> TransportPacket:
55
+ """
56
+ Execute the orchestrator workflow and return a canonical response packet.
57
+ """
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from constellation_node_sdk.transport.packet import TransportPacket
6
+
7
+
8
+ def _payload_copy(packet: TransportPacket) -> dict[str, Any]:
9
+ return dict(packet.payload)
10
+
11
+
12
+ def _response_payload(response: TransportPacket) -> dict[str, Any]:
13
+ return dict(response.payload)
14
+
15
+
16
+ def merge_identity(current: TransportPacket, response: TransportPacket) -> dict[str, Any]:
17
+ """
18
+ Replace accumulated payload with the full response payload.
19
+ """
20
+ del current
21
+ return _response_payload(response)
22
+
23
+
24
+ def merge_results(current: TransportPacket, response: TransportPacket) -> dict[str, Any]:
25
+ """
26
+ Merge response.payload["data"] into current payload when present.
27
+ Fall back to merging the full response payload.
28
+ """
29
+ merged = _payload_copy(current)
30
+ response_payload = _response_payload(response)
31
+
32
+ response_data = response_payload.get("data")
33
+ if isinstance(response_data, dict):
34
+ merged.update(response_data)
35
+ return merged
36
+
37
+ merged.update(response_payload)
38
+ return merged
39
+
40
+
41
+ def merge_payload(current: TransportPacket, response: TransportPacket) -> dict[str, Any]:
42
+ """
43
+ Merge the full response payload into current payload.
44
+ """
45
+ merged = _payload_copy(current)
46
+ merged.update(_response_payload(response))
47
+ return merged
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from constellation_node_sdk.transport.packet import TransportPacket
6
+ from constellation_node_sdk.transport.provenance import RoutingProvenance
7
+
8
+
9
+ def build_step_packet(
10
+ *,
11
+ parent: TransportPacket,
12
+ action: str,
13
+ payload: dict[str, Any],
14
+ source_node: str,
15
+ reply_to: str,
16
+ ) -> TransportPacket:
17
+ """
18
+ Build a child step packet for orchestrator -> Gate execution.
19
+
20
+ This function enforces the architectural constraint that every step packet
21
+ emitted by an orchestrator targets Gate, never a peer node.
22
+ """
23
+ normalized_action = action.strip().lower()
24
+ if not normalized_action:
25
+ raise ValueError("action must not be empty")
26
+
27
+ normalized_source = source_node.strip().lower()
28
+ normalized_reply_to = reply_to.strip().lower()
29
+ if not normalized_source:
30
+ raise ValueError("source_node must not be empty")
31
+ if not normalized_reply_to:
32
+ raise ValueError("reply_to must not be empty")
33
+
34
+ provenance = RoutingProvenance(
35
+ origin_kind="node",
36
+ requested_action=normalized_action,
37
+ resolved_by_gate=False,
38
+ original_source_node=normalized_source,
39
+ )
40
+
41
+ return parent.derive(
42
+ packet_type="request",
43
+ action=normalized_action,
44
+ source_node=normalized_source,
45
+ destination_node="gate",
46
+ reply_to=normalized_reply_to,
47
+ payload=dict(payload),
48
+ provenance=provenance,
49
+ )
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
4
+
5
+
6
+ class RetryPolicy(BaseModel):
7
+ """
8
+ Deterministic retry policy for orchestrator step execution.
9
+ """
10
+
11
+ model_config = ConfigDict(frozen=True, extra="forbid")
12
+
13
+ max_attempts: int = Field(default=3, ge=1)
14
+ initial_delay_seconds: float = Field(default=0.25, ge=0.0)
15
+ backoff_multiplier: float = Field(default=2.0, ge=1.0)
16
+ max_delay_seconds: float = Field(default=5.0, ge=0.0)
17
+ retryable_error_types: tuple[str, ...] = (
18
+ "TimeoutError",
19
+ "TransportAuthenticationError",
20
+ "TransportAuthorizationError",
21
+ "TransportValidationError",
22
+ "TransportIntegrityError",
23
+ "GateClientError",
24
+ )
25
+
26
+ @field_validator("retryable_error_types")
27
+ @classmethod
28
+ def validate_retryable_error_types(cls, value: tuple[str, ...]) -> tuple[str, ...]:
29
+ normalized = tuple(item.strip() for item in value if item.strip())
30
+ if len(set(normalized)) != len(normalized):
31
+ raise ValueError("retryable_error_types must not contain duplicates")
32
+ return normalized
33
+
34
+ def delay_for_attempt(self, attempt: int) -> float:
35
+ """
36
+ Return the backoff delay for the next retry after `attempt`.
37
+ """
38
+ if attempt < 1:
39
+ raise ValueError("attempt must be >= 1")
40
+ if self.max_attempts <= 1:
41
+ return 0.0
42
+
43
+ delay = self.initial_delay_seconds * (self.backoff_multiplier ** (attempt - 1))
44
+ return min(delay, self.max_delay_seconds)
45
+
46
+
47
+ def should_retry(*, attempt: int, error: Exception, policy: RetryPolicy) -> bool:
48
+ """
49
+ Return True when the given error should be retried under the policy.
50
+
51
+ `attempt` is 1-indexed and represents the attempt that just failed.
52
+ """
53
+ if attempt < 1:
54
+ raise ValueError("attempt must be >= 1")
55
+
56
+ if attempt >= policy.max_attempts:
57
+ return False
58
+
59
+ error_type_names = {cls.__name__ for cls in type(error).mro()}
60
+ retryable = set(policy.retryable_error_types)
61
+ return bool(error_type_names & retryable)
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from uuid import UUID
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ from constellation_node_sdk.transport.packet import TransportPacket
9
+
10
+
11
+ class OrchestratorState(BaseModel):
12
+ """
13
+ Immutable-ish workflow state container for orchestrators.
14
+
15
+ The orchestrator owns workflow-local accumulation state. Gate owns routing.
16
+ """
17
+
18
+ model_config = ConfigDict(frozen=True, extra="forbid")
19
+
20
+ root_id: UUID
21
+ current_packet_id: UUID
22
+ current_generation: int = Field(ge=0)
23
+ orchestrator_node: str
24
+ workflow_name: str | None = None
25
+ accumulated_payload: dict[str, Any] = Field(default_factory=dict)
26
+ step_results: tuple[dict[str, Any], ...] = ()
27
+ completed_steps: tuple[str, ...] = ()
28
+ failed_steps: tuple[str, ...] = ()
29
+
30
+ @classmethod
31
+ def from_packet(
32
+ cls,
33
+ *,
34
+ packet: TransportPacket,
35
+ orchestrator_node: str,
36
+ workflow_name: str | None = None,
37
+ ) -> OrchestratorState:
38
+ return cls(
39
+ root_id=packet.lineage.root_id,
40
+ current_packet_id=packet.header.packet_id,
41
+ current_generation=packet.lineage.generation,
42
+ orchestrator_node=orchestrator_node.strip().lower(),
43
+ workflow_name=None if workflow_name is None else workflow_name.strip(),
44
+ accumulated_payload=dict(packet.payload),
45
+ step_results=(),
46
+ completed_steps=(),
47
+ failed_steps=(),
48
+ )
49
+
50
+ def with_step_success(
51
+ self,
52
+ *,
53
+ step_name: str,
54
+ response_packet: TransportPacket,
55
+ merged_payload: dict[str, Any],
56
+ ) -> OrchestratorState:
57
+ response_snapshot = {
58
+ "step_name": step_name.strip().lower(),
59
+ "packet_id": str(response_packet.header.packet_id),
60
+ "action": response_packet.header.action,
61
+ "payload": dict(response_packet.payload),
62
+ }
63
+ return self.model_copy(
64
+ update={
65
+ "current_packet_id": response_packet.header.packet_id,
66
+ "current_generation": response_packet.lineage.generation,
67
+ "accumulated_payload": dict(merged_payload),
68
+ "step_results": self.step_results + (response_snapshot,),
69
+ "completed_steps": self.completed_steps + (step_name.strip().lower(),),
70
+ }
71
+ )
72
+
73
+ def with_step_failure(self, *, step_name: str) -> OrchestratorState:
74
+ return self.model_copy(
75
+ update={
76
+ "failed_steps": self.failed_steps + (step_name.strip().lower(),),
77
+ }
78
+ )
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from constellation_node_sdk.gate.client import GateClient
7
+ from constellation_node_sdk.transport.packet import TransportPacket
8
+
9
+ from .packet_builder import build_step_packet
10
+ from .retry import RetryPolicy, should_retry
11
+
12
+
13
+ class StepExecutionError(RuntimeError):
14
+ """
15
+ Raised when an orchestrator step cannot be completed successfully.
16
+ """
17
+
18
+
19
+ class StepExecutor:
20
+ """
21
+ Execute atomic workflow steps by sending child TransportPackets to Gate.
22
+
23
+ This class never resolves peer nodes directly. It relies entirely on Gate
24
+ to route the requested action.
25
+ """
26
+
27
+ def __init__(self, *, gate_client: GateClient, source_node: str) -> None:
28
+ normalized_source = source_node.strip().lower()
29
+ if not normalized_source:
30
+ raise ValueError("source_node must not be empty")
31
+
32
+ self._gate_client = gate_client
33
+ self._source_node = normalized_source
34
+
35
+ @property
36
+ def source_node(self) -> str:
37
+ return self._source_node
38
+
39
+ async def execute_step(
40
+ self,
41
+ *,
42
+ parent: TransportPacket,
43
+ action: str,
44
+ payload: dict[str, Any],
45
+ retry_policy: RetryPolicy | None = None,
46
+ ) -> TransportPacket:
47
+ """
48
+ Execute a single orchestrator step through Gate with optional retries.
49
+ """
50
+ policy = retry_policy or RetryPolicy()
51
+ last_error: Exception | None = None
52
+
53
+ for attempt in range(1, policy.max_attempts + 1):
54
+ step_packet = build_step_packet(
55
+ parent=parent,
56
+ action=action,
57
+ payload=payload,
58
+ source_node=self._source_node,
59
+ reply_to=self._source_node,
60
+ )
61
+
62
+ try:
63
+ response = await self._gate_client.send_to_gate(step_packet)
64
+ _ensure_success_response(response)
65
+ return response
66
+ except Exception as exc: # noqa: BLE001
67
+ last_error = exc
68
+ if not should_retry(attempt=attempt, error=exc, policy=policy):
69
+ break
70
+ delay = policy.delay_for_attempt(attempt)
71
+ if delay > 0:
72
+ await asyncio.sleep(delay)
73
+
74
+ message = f"step execution failed for action={action!r}"
75
+ if last_error is not None:
76
+ raise StepExecutionError(message) from last_error
77
+ raise StepExecutionError(message)
78
+
79
+
80
+ def _ensure_success_response(packet: TransportPacket) -> None:
81
+ packet_type = packet.header.packet_type
82
+ if packet_type == "failure":
83
+ raise StepExecutionError("Gate returned a failure packet")
84
+
85
+ payload_status = str(packet.payload.get("status", "completed")).strip().lower()
86
+ if payload_status in {"failed", "failure", "error"}:
87
+ raise StepExecutionError("step response payload indicates failure")
File without changes
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from constellation_node_sdk.runtime.app import create_node_app
4
+ from constellation_node_sdk.runtime.config import NodeRuntimeConfig, get_runtime_config
5
+ from constellation_node_sdk.runtime.errors import (
6
+ RuntimeConfigurationError,
7
+ RuntimeErrorDetail,
8
+ RuntimeExecutionError,
9
+ RuntimeValidationError,
10
+ classify_exception,
11
+ raise_http_exception,
12
+ )
13
+ from constellation_node_sdk.runtime.execution import (
14
+ create_error_transport_packet,
15
+ execute_transport_packet,
16
+ )
17
+ from constellation_node_sdk.runtime.handlers import (
18
+ clear_handlers,
19
+ get_handler,
20
+ register_handler,
21
+ registered_actions,
22
+ )
23
+ from constellation_node_sdk.runtime.inbound_policy import (
24
+ validate_execute_ingress_packet,
25
+ validate_relay_ingress_packet,
26
+ )
27
+ from constellation_node_sdk.runtime.lifecycle import LifecycleHook, NoOpLifecycle
28
+ from constellation_node_sdk.runtime.preflight import PreflightFailure, run_preflight
29
+
30
+ __all__ = [
31
+ "LifecycleHook",
32
+ "NoOpLifecycle",
33
+ "NodeRuntimeConfig",
34
+ "PreflightFailure",
35
+ "RuntimeConfigurationError",
36
+ "RuntimeErrorDetail",
37
+ "RuntimeExecutionError",
38
+ "RuntimeValidationError",
39
+ "classify_exception",
40
+ "clear_handlers",
41
+ "create_error_transport_packet",
42
+ "create_node_app",
43
+ "execute_transport_packet",
44
+ "validate_execute_ingress_packet",
45
+ "validate_relay_ingress_packet",
46
+ "get_handler",
47
+ "get_runtime_config",
48
+ "raise_http_exception",
49
+ "register_handler",
50
+ "registered_actions",
51
+ "run_preflight",
52
+ ]