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.
- constellation_node_sdk/__init__.py +123 -0
- constellation_node_sdk/gate/__init__.py +47 -0
- constellation_node_sdk/gate/client.py +108 -0
- constellation_node_sdk/gate/config.py +153 -0
- constellation_node_sdk/gate/errors.py +17 -0
- constellation_node_sdk/gate/policy.py +70 -0
- constellation_node_sdk/gate/registration.py +144 -0
- constellation_node_sdk/orchestrator/__init__.py +3 -0
- constellation_node_sdk/orchestrator/base.py +57 -0
- constellation_node_sdk/orchestrator/merge.py +47 -0
- constellation_node_sdk/orchestrator/packet_builder.py +49 -0
- constellation_node_sdk/orchestrator/retry.py +61 -0
- constellation_node_sdk/orchestrator/state.py +78 -0
- constellation_node_sdk/orchestrator/step_executor.py +87 -0
- constellation_node_sdk/py.typed +0 -0
- constellation_node_sdk/runtime/__init__.py +52 -0
- constellation_node_sdk/runtime/app.py +274 -0
- constellation_node_sdk/runtime/config.py +307 -0
- constellation_node_sdk/runtime/errors.py +112 -0
- constellation_node_sdk/runtime/execution.py +249 -0
- constellation_node_sdk/runtime/handlers.py +45 -0
- constellation_node_sdk/runtime/inbound_policy.py +86 -0
- constellation_node_sdk/runtime/lifecycle.py +29 -0
- constellation_node_sdk/runtime/observability.py +72 -0
- constellation_node_sdk/runtime/preflight.py +122 -0
- constellation_node_sdk/security/__init__.py +36 -0
- constellation_node_sdk/security/delegation.py +57 -0
- constellation_node_sdk/security/errors.py +25 -0
- constellation_node_sdk/security/signing.py +121 -0
- constellation_node_sdk/security/validation.py +230 -0
- constellation_node_sdk/security/verification.py +107 -0
- constellation_node_sdk/transport/__init__.py +88 -0
- constellation_node_sdk/transport/codec.py +21 -0
- constellation_node_sdk/transport/errors.py +41 -0
- constellation_node_sdk/transport/hashing.py +73 -0
- constellation_node_sdk/transport/hop_trace.py +378 -0
- constellation_node_sdk/transport/lineage.py +28 -0
- constellation_node_sdk/transport/models.py +447 -0
- constellation_node_sdk/transport/packet.py +331 -0
- constellation_node_sdk/transport/provenance.py +53 -0
- constellation_node_sdk/transport/tenant.py +76 -0
- constellation_node_sdk-1.0.1.dist-info/METADATA +141 -0
- constellation_node_sdk-1.0.1.dist-info/RECORD +45 -0
- constellation_node_sdk-1.0.1.dist-info/WHEEL +5 -0
- 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
|
+
]
|