lumera 0.41.2.dev20260909__tar.gz → 0.41.3.dev2026091001__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.
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/PKG-INFO +1 -1
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/function_resources.py +4 -25
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/__init__.py +8 -9
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_artifacts.py +1 -1
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_context.py +3 -3
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_errors.py +4 -8
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_execution.py +30 -25
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_remote.py +59 -32
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_runner.py +19 -1
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_runtime.py +15 -16
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/testing.py +5 -5
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/__init__.py +1 -2
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/__main__.py +2 -2
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_api_child.py +68 -26
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_projection.py +7 -2
- lumera-0.41.3.dev2026091001/lumera/releases/_rpc_event.py +233 -0
- lumera-0.41.3.dev2026091001/lumera/releases/_rpc_process.py +266 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_runtime.py +2 -2
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_runtime_child.py +1 -1
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/PKG-INFO +1 -1
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/SOURCES.txt +2 -4
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/pyproject.toml +1 -1
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_function_resources.py +3 -23
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_lazy_imports.py +0 -23
- lumera-0.41.2.dev20260909/lumera/releases/_durable_event.py +0 -378
- lumera-0.41.2.dev20260909/lumera/releases/_function_child.py +0 -217
- lumera-0.41.2.dev20260909/lumera/releases/_worker_child.py +0 -459
- lumera-0.41.2.dev20260909/lumera/releases/_worker_protocol.py +0 -131
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/__init__.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/_live.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/_utils.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/agents.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/automations.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/email.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/exceptions.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/files.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/flags.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/__main__.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_config.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_contract.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_dependencies.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_jsonschema.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_registry.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/google.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/__init__.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/bill.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/finch.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/google.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/kafka.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/netsuite.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/salesforce.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/slack.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/snowflake.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/llm.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/pb.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_archive.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_builder.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_config.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_contracts.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_errors.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_schemas.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_target.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/sdk.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/storage.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/webhooks.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/dependency_links.txt +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/requires.txt +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/top_level.txt +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/setup.cfg +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_agents_live.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_automations.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_flags.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_bill.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_finch.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_google.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_kafka.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_netsuite.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_salesforce.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_snowflake.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_sdk.py +0 -0
- {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_webhooks.py +0 -0
|
@@ -9,6 +9,8 @@ from urllib.parse import quote
|
|
|
9
9
|
|
|
10
10
|
from ._utils import _api_request
|
|
11
11
|
|
|
12
|
+
_FUNCTION_INVOCATION_TIMEOUT_SECONDS = 295
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
def deploy_release(
|
|
14
16
|
project: str,
|
|
@@ -104,6 +106,7 @@ def invoke_function(
|
|
|
104
106
|
"POST",
|
|
105
107
|
f"projects/{quote(project_ref, safe='')}/functions/{quote(function_ref, safe='')}/invoke",
|
|
106
108
|
json_body=body,
|
|
109
|
+
timeout=_FUNCTION_INVOCATION_TIMEOUT_SECONDS,
|
|
107
110
|
)
|
|
108
111
|
return _mapping(payload, "Function invocation")
|
|
109
112
|
|
|
@@ -117,8 +120,6 @@ def list_function_executions(
|
|
|
117
120
|
def get_function_execution(
|
|
118
121
|
project: str,
|
|
119
122
|
execution: str,
|
|
120
|
-
*,
|
|
121
|
-
wait_seconds: int | None = None,
|
|
122
123
|
) -> dict[str, Any]:
|
|
123
124
|
project_ref = _required_ref(project, "project")
|
|
124
125
|
execution_ref = _required_ref(execution, "execution")
|
|
@@ -126,31 +127,10 @@ def get_function_execution(
|
|
|
126
127
|
f"projects/{quote(project_ref, safe='')}/function-executions/"
|
|
127
128
|
f"{quote(execution_ref, safe='')}"
|
|
128
129
|
)
|
|
129
|
-
|
|
130
|
-
if wait_seconds is not None:
|
|
131
|
-
if isinstance(wait_seconds, bool) or not 1 <= wait_seconds <= 30:
|
|
132
|
-
raise ValueError("wait_seconds must be between 1 and 30")
|
|
133
|
-
options["request_headers"] = {"Prefer": f"wait={wait_seconds}"}
|
|
134
|
-
options["timeout"] = wait_seconds + 5
|
|
135
|
-
payload = _api_request("GET", path, **options)
|
|
130
|
+
payload = _api_request("GET", path)
|
|
136
131
|
return _mapping(payload, "Function execution")
|
|
137
132
|
|
|
138
133
|
|
|
139
|
-
def cancel_function_execution(project: str, execution: str) -> dict[str, Any]:
|
|
140
|
-
project_ref = _required_ref(project, "project")
|
|
141
|
-
execution_ref = _required_ref(execution, "execution")
|
|
142
|
-
path = (
|
|
143
|
-
f"projects/{quote(project_ref, safe='')}/function-executions/"
|
|
144
|
-
f"{quote(execution_ref, safe='')}/cancel"
|
|
145
|
-
)
|
|
146
|
-
payload = _api_request(
|
|
147
|
-
"POST",
|
|
148
|
-
path,
|
|
149
|
-
json_body={},
|
|
150
|
-
)
|
|
151
|
-
return _mapping(payload, "Function execution cancellation")
|
|
152
|
-
|
|
153
|
-
|
|
154
134
|
def _list(kind: str, project: str, *, cursor: str | None, limit: int) -> dict[str, Any]:
|
|
155
135
|
project_ref = _required_ref(project, "project")
|
|
156
136
|
payload = _api_request(
|
|
@@ -184,7 +164,6 @@ def _mapping(payload: object | None, kind: str) -> dict[str, Any]:
|
|
|
184
164
|
|
|
185
165
|
|
|
186
166
|
__all__ = [
|
|
187
|
-
"cancel_function_execution",
|
|
188
167
|
"deploy_release",
|
|
189
168
|
"deploy_runtime",
|
|
190
169
|
"get_function",
|
|
@@ -29,8 +29,6 @@ from ._contract import (
|
|
|
29
29
|
from ._errors import (
|
|
30
30
|
DuplicateFunctionError,
|
|
31
31
|
DuplicateHttpProjectionError,
|
|
32
|
-
DurableTransportUnavailableError,
|
|
33
|
-
DurableWaitNotAllowedError,
|
|
34
32
|
FunctionCallModeError,
|
|
35
33
|
FunctionCancelledError,
|
|
36
34
|
FunctionCycleError,
|
|
@@ -44,18 +42,19 @@ from ._errors import (
|
|
|
44
42
|
FunctionOutputSerializationError,
|
|
45
43
|
FunctionRuntimeClosedError,
|
|
46
44
|
FunctionsError,
|
|
45
|
+
FunctionTransportUnavailableError,
|
|
47
46
|
FunctionValidationError,
|
|
48
|
-
|
|
47
|
+
InvalidFunctionWaitError,
|
|
49
48
|
UnexpectedFunctionError,
|
|
50
49
|
)
|
|
51
50
|
from ._execution import (
|
|
52
|
-
DurableFunctionTransport,
|
|
53
51
|
ExecutionHandle,
|
|
54
52
|
ExecutionStatus,
|
|
53
|
+
FunctionTransport,
|
|
55
54
|
StartedExecution,
|
|
56
55
|
)
|
|
57
56
|
from ._registry import FunctionRegistry, discover_modules
|
|
58
|
-
from ._remote import RemoteFunctionTransport, invoke_remote
|
|
57
|
+
from ._remote import RemoteFunctionTransport, call_remote, invoke_remote
|
|
59
58
|
from ._runtime import LocalFunctionRuntime, invoke_local
|
|
60
59
|
|
|
61
60
|
del _dependencies
|
|
@@ -68,9 +67,6 @@ __all__ = [
|
|
|
68
67
|
"CancellationToken",
|
|
69
68
|
"DuplicateFunctionError",
|
|
70
69
|
"DuplicateHttpProjectionError",
|
|
71
|
-
"DurableFunctionTransport",
|
|
72
|
-
"DurableTransportUnavailableError",
|
|
73
|
-
"DurableWaitNotAllowedError",
|
|
74
70
|
"ExecutionHandle",
|
|
75
71
|
"ExecutionStatus",
|
|
76
72
|
"FUNCTION_MANIFEST_SCHEMA_VERSION",
|
|
@@ -94,12 +90,15 @@ __all__ = [
|
|
|
94
90
|
"FunctionOutputSerializationError",
|
|
95
91
|
"FunctionRegistry",
|
|
96
92
|
"FunctionRuntimeClosedError",
|
|
93
|
+
"FunctionTransport",
|
|
94
|
+
"FunctionTransportUnavailableError",
|
|
97
95
|
"FunctionsError",
|
|
98
96
|
"FunctionValidationError",
|
|
99
97
|
"HttpProjection",
|
|
100
|
-
"
|
|
98
|
+
"InvalidFunctionWaitError",
|
|
101
99
|
"LocalFunctionRuntime",
|
|
102
100
|
"RemoteFunctionTransport",
|
|
101
|
+
"call_remote",
|
|
103
102
|
"ScopedLumeraClient",
|
|
104
103
|
"StartedExecution",
|
|
105
104
|
"StructuredLogger",
|
|
@@ -10,7 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
10
10
|
class ArtifactReference(BaseModel):
|
|
11
11
|
"""An opaque, non-authoritative reference to a Lumera-managed artifact.
|
|
12
12
|
|
|
13
|
-
Function payloads carry this JSON-safe envelope across local, API,
|
|
13
|
+
Function payloads carry this JSON-safe envelope across local, API, remote,
|
|
14
14
|
and sharing boundaries. Runtime adapters are responsible for authorizing
|
|
15
15
|
and materializing the referenced artifact. Local paths, object-store keys,
|
|
16
16
|
URLs, and credentials are deliberately absent from the contract.
|
|
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|
|
25
25
|
from ._execution import ExecutionHandle
|
|
26
26
|
|
|
27
27
|
ResultT = TypeVar("ResultT")
|
|
28
|
-
RuntimeMode = Literal["local", "api", "
|
|
28
|
+
RuntimeMode = Literal["local", "api", "external"]
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass(frozen=True)
|
|
@@ -159,7 +159,7 @@ class LocalScopedLumeraClient:
|
|
|
159
159
|
|
|
160
160
|
@runtime_checkable
|
|
161
161
|
class FunctionCaller(Protocol):
|
|
162
|
-
"""Validated same-release and
|
|
162
|
+
"""Validated same-release and remote Function composition interface."""
|
|
163
163
|
|
|
164
164
|
@overload
|
|
165
165
|
async def call(
|
|
@@ -388,7 +388,7 @@ def create_test_context(
|
|
|
388
388
|
) -> TestFunctionContext:
|
|
389
389
|
"""Build a deterministic-shape context without platform credentials."""
|
|
390
390
|
|
|
391
|
-
if runtime_mode not in {"local", "api", "
|
|
391
|
+
if runtime_mode not in {"local", "api", "external"}:
|
|
392
392
|
raise ValueError(f"unsupported Function runtime mode: {runtime_mode!r}")
|
|
393
393
|
resolved_deadline = deadline or (datetime.now(UTC) + timedelta(minutes=5))
|
|
394
394
|
if resolved_deadline.tzinfo is None:
|
|
@@ -158,13 +158,9 @@ class FunctionCallModeError(FunctionInvocationError):
|
|
|
158
158
|
"""The caller used a sync composition API from an unsafe thread."""
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
class
|
|
162
|
-
"""No
|
|
161
|
+
class FunctionTransportUnavailableError(FunctionsError):
|
|
162
|
+
"""No remote Function transport is configured in this context."""
|
|
163
163
|
|
|
164
164
|
|
|
165
|
-
class
|
|
166
|
-
"""A Function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class InvalidDurableWaitError(FunctionsError):
|
|
170
|
-
"""A durable result wait was not positive and bounded."""
|
|
165
|
+
class InvalidFunctionWaitError(FunctionsError):
|
|
166
|
+
"""A Function result wait was not positive and bounded."""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Function RPC execution handles and transport interfaces."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -8,14 +8,14 @@ from dataclasses import dataclass
|
|
|
8
8
|
from typing import Any, Generic, Protocol, TypeVar
|
|
9
9
|
|
|
10
10
|
from ._context import FunctionContext
|
|
11
|
-
from ._errors import
|
|
11
|
+
from ._errors import InvalidFunctionWaitError
|
|
12
12
|
|
|
13
13
|
ResultT = TypeVar("ResultT")
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass(frozen=True)
|
|
17
17
|
class ExecutionStatus:
|
|
18
|
-
"""Transport-neutral
|
|
18
|
+
"""Transport-neutral Function execution status snapshot."""
|
|
19
19
|
|
|
20
20
|
execution_id: str
|
|
21
21
|
status: str
|
|
@@ -25,14 +25,16 @@ class ExecutionStatus:
|
|
|
25
25
|
|
|
26
26
|
@dataclass(frozen=True)
|
|
27
27
|
class StartedExecution:
|
|
28
|
-
"""
|
|
28
|
+
"""Execution snapshot returned by a Function transport."""
|
|
29
29
|
|
|
30
30
|
execution_id: str
|
|
31
|
-
status: str = "
|
|
31
|
+
status: str = "running"
|
|
32
|
+
result: Any | None = None
|
|
33
|
+
error: Mapping[str, Any] | None = None
|
|
32
34
|
|
|
33
35
|
|
|
34
|
-
class
|
|
35
|
-
"""Backend adapter
|
|
36
|
+
class FunctionTransport(Protocol):
|
|
37
|
+
"""Backend adapter used for synchronous remote Function calls."""
|
|
36
38
|
|
|
37
39
|
async def start(
|
|
38
40
|
self,
|
|
@@ -56,9 +58,8 @@ class DurableFunctionTransport(Protocol):
|
|
|
56
58
|
class ExecutionHandle(Generic[ResultT]):
|
|
57
59
|
"""Handle returned by ``ctx.functions.start``.
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
waiting for queued child work.
|
|
61
|
+
The RPC has normally finished before this handle is returned. ``result``
|
|
62
|
+
remains bounded for compatibility with handles created by custom transports.
|
|
62
63
|
"""
|
|
63
64
|
|
|
64
65
|
def __init__(
|
|
@@ -67,39 +68,43 @@ class ExecutionHandle(Generic[ResultT]):
|
|
|
67
68
|
execution_id: str,
|
|
68
69
|
function_ref: str,
|
|
69
70
|
initial_status: str,
|
|
70
|
-
transport:
|
|
71
|
-
|
|
71
|
+
transport: FunctionTransport,
|
|
72
|
+
initial_result: object | None = None,
|
|
73
|
+
initial_error: Mapping[str, Any] | None = None,
|
|
72
74
|
result_validator: Callable[[object], ResultT] | None = None,
|
|
73
75
|
) -> None:
|
|
74
76
|
self.execution_id = execution_id
|
|
75
77
|
self.function_ref = function_ref
|
|
76
78
|
self.initial_status = initial_status
|
|
77
79
|
self._transport = transport
|
|
78
|
-
self.
|
|
80
|
+
self._initial_result = initial_result
|
|
81
|
+
self._initial_error = initial_error
|
|
79
82
|
self._result_validator = result_validator
|
|
80
83
|
|
|
81
84
|
async def status(self) -> ExecutionStatus:
|
|
82
|
-
"""Read the latest
|
|
85
|
+
"""Read the latest Function execution status."""
|
|
83
86
|
|
|
84
87
|
return await self._transport.get_status(self.execution_id)
|
|
85
88
|
|
|
86
89
|
async def result(self, *, timeout: float) -> ResultT:
|
|
87
90
|
"""Wait for at most ``timeout`` seconds and return the transport result."""
|
|
88
91
|
|
|
89
|
-
if not self._wait_allowed:
|
|
90
|
-
raise DurableWaitNotAllowedError(
|
|
91
|
-
"Function workers may start durable child work but cannot wait for it; "
|
|
92
|
-
"use same-release inline call/call_sync for an immediate result, or "
|
|
93
|
-
"move fan-out and waiting to a Workflow"
|
|
94
|
-
)
|
|
95
92
|
if not math.isfinite(timeout) or timeout <= 0:
|
|
96
|
-
raise
|
|
93
|
+
raise InvalidFunctionWaitError(
|
|
97
94
|
"ExecutionHandle.result requires a positive, finite timeout"
|
|
98
95
|
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
if self.initial_status in {"failed", "indeterminate", "timed_out"}:
|
|
97
|
+
raise RuntimeError(
|
|
98
|
+
f"Function execution {self.execution_id} ended as "
|
|
99
|
+
f"{self.initial_status}: {self._initial_error}"
|
|
100
|
+
)
|
|
101
|
+
if self.initial_status == "succeeded":
|
|
102
|
+
result = self._initial_result
|
|
103
|
+
else:
|
|
104
|
+
result = await self._transport.get_result(
|
|
105
|
+
self.execution_id,
|
|
106
|
+
timeout=timeout,
|
|
107
|
+
)
|
|
103
108
|
if self._result_validator is not None:
|
|
104
109
|
return self._result_validator(result)
|
|
105
110
|
# Unknown/imported references are validated by the future authoritative
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Synchronous backend Function transport and execution handles."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -8,13 +8,12 @@ from collections.abc import Mapping
|
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
10
|
from ._context import FunctionContext
|
|
11
|
-
from ._execution import
|
|
11
|
+
from ._execution import ExecutionHandle, ExecutionStatus, FunctionTransport, StartedExecution
|
|
12
12
|
|
|
13
|
-
_TERMINAL = {"succeeded", "failed", "indeterminate", "timed_out"
|
|
14
|
-
_MAX_LONG_POLL_SECONDS = 20
|
|
13
|
+
_TERMINAL = {"succeeded", "failed", "indeterminate", "timed_out"}
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
class RemoteFunctionTransport(
|
|
16
|
+
class RemoteFunctionTransport(FunctionTransport):
|
|
18
17
|
def __init__(self, project: str) -> None:
|
|
19
18
|
self.project = project
|
|
20
19
|
|
|
@@ -37,35 +36,27 @@ class RemoteFunctionTransport(DurableFunctionTransport):
|
|
|
37
36
|
parent_execution_id=context.execution_id,
|
|
38
37
|
)
|
|
39
38
|
execution = _execution(response)
|
|
40
|
-
|
|
39
|
+
status = _status(execution)
|
|
40
|
+
return StartedExecution(
|
|
41
|
+
execution_id=status.execution_id,
|
|
42
|
+
status=status.status,
|
|
43
|
+
result=status.result,
|
|
44
|
+
error=status.error,
|
|
45
|
+
)
|
|
41
46
|
|
|
42
47
|
async def get_status(self, execution_id: str) -> ExecutionStatus:
|
|
43
48
|
return await self._get_status(execution_id)
|
|
44
49
|
|
|
45
|
-
async def _get_status(
|
|
46
|
-
self, execution_id: str, *, wait_seconds: int | None = None
|
|
47
|
-
) -> ExecutionStatus:
|
|
50
|
+
async def _get_status(self, execution_id: str) -> ExecutionStatus:
|
|
48
51
|
from lumera import function_resources
|
|
49
52
|
|
|
50
53
|
response = await asyncio.to_thread(
|
|
51
54
|
function_resources.get_function_execution,
|
|
52
55
|
self.project,
|
|
53
56
|
execution_id,
|
|
54
|
-
wait_seconds=wait_seconds,
|
|
55
57
|
)
|
|
56
58
|
execution = _execution(response)
|
|
57
|
-
|
|
58
|
-
if execution.get("error_code"):
|
|
59
|
-
error = {
|
|
60
|
-
"code": execution["error_code"],
|
|
61
|
-
"message": execution.get("error_message", ""),
|
|
62
|
-
}
|
|
63
|
-
return ExecutionStatus(
|
|
64
|
-
execution_id=str(execution["id"]),
|
|
65
|
-
status=str(execution["status"]),
|
|
66
|
-
result=execution.get("result"),
|
|
67
|
-
error=error,
|
|
68
|
-
)
|
|
59
|
+
return _status(execution)
|
|
69
60
|
|
|
70
61
|
async def get_result(self, execution_id: str, *, timeout: float) -> object:
|
|
71
62
|
deadline = time.monotonic() + timeout
|
|
@@ -73,12 +64,8 @@ class RemoteFunctionTransport(DurableFunctionTransport):
|
|
|
73
64
|
remaining = deadline - time.monotonic()
|
|
74
65
|
if remaining <= 0:
|
|
75
66
|
raise TimeoutError(f"Function execution {execution_id} did not finish in time")
|
|
76
|
-
wait_seconds = min(_MAX_LONG_POLL_SECONDS, int(remaining))
|
|
77
67
|
request_started = time.monotonic()
|
|
78
|
-
status = await self._get_status(
|
|
79
|
-
execution_id,
|
|
80
|
-
wait_seconds=wait_seconds if wait_seconds >= 1 else None,
|
|
81
|
-
)
|
|
68
|
+
status = await self._get_status(execution_id)
|
|
82
69
|
if status.status in _TERMINAL:
|
|
83
70
|
if status.status != "succeeded":
|
|
84
71
|
raise RuntimeError(
|
|
@@ -89,9 +76,8 @@ class RemoteFunctionTransport(DurableFunctionTransport):
|
|
|
89
76
|
remaining = deadline - time.monotonic()
|
|
90
77
|
if remaining <= 0:
|
|
91
78
|
raise TimeoutError(f"Function execution {execution_id} did not finish in time")
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
# endpoint; completion-aware backends normally hold this request.
|
|
79
|
+
# A concurrent idempotent caller can observe the first RPC in flight.
|
|
80
|
+
# Poll coarsely rather than hammering the status endpoint.
|
|
95
81
|
request_elapsed = time.monotonic() - request_started
|
|
96
82
|
await asyncio.sleep(min(max(0.0, 1.0 - request_elapsed), remaining))
|
|
97
83
|
|
|
@@ -114,8 +100,31 @@ def invoke_remote(
|
|
|
114
100
|
function_ref=function_ref,
|
|
115
101
|
initial_status=str(execution["status"]),
|
|
116
102
|
transport=RemoteFunctionTransport(project),
|
|
117
|
-
|
|
103
|
+
initial_result=execution.get("result"),
|
|
104
|
+
initial_error=_execution_error(execution),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def call_remote(
|
|
109
|
+
project: str,
|
|
110
|
+
function_ref: str,
|
|
111
|
+
input: Mapping[str, Any] | None = None,
|
|
112
|
+
*,
|
|
113
|
+
idempotency_key: str | None = None,
|
|
114
|
+
) -> object:
|
|
115
|
+
"""Call a deployed Function synchronously and return its result."""
|
|
116
|
+
from lumera import function_resources
|
|
117
|
+
|
|
118
|
+
response = function_resources.invoke_function(
|
|
119
|
+
project, function_ref, input, idempotency_key=idempotency_key
|
|
118
120
|
)
|
|
121
|
+
execution = _execution(response)
|
|
122
|
+
status = _status(execution)
|
|
123
|
+
if status.status != "succeeded":
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
f"Function execution {status.execution_id} ended as {status.status}: {status.error}"
|
|
126
|
+
)
|
|
127
|
+
return status.result
|
|
119
128
|
|
|
120
129
|
|
|
121
130
|
def _execution(response: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
@@ -125,4 +134,22 @@ def _execution(response: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
|
125
134
|
return execution
|
|
126
135
|
|
|
127
136
|
|
|
128
|
-
|
|
137
|
+
def _execution_error(execution: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
|
138
|
+
if not execution.get("error_code"):
|
|
139
|
+
return None
|
|
140
|
+
return {
|
|
141
|
+
"code": execution["error_code"],
|
|
142
|
+
"message": execution.get("error_message", ""),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _status(execution: Mapping[str, Any]) -> ExecutionStatus:
|
|
147
|
+
return ExecutionStatus(
|
|
148
|
+
execution_id=str(execution["id"]),
|
|
149
|
+
status=str(execution["status"]),
|
|
150
|
+
result=execution.get("result"),
|
|
151
|
+
error=_execution_error(execution),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
__all__ = ["RemoteFunctionTransport", "call_remote", "invoke_remote"]
|
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
11
|
import uuid
|
|
12
|
-
from collections.abc import Iterator, Sequence
|
|
12
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
13
13
|
from contextlib import contextmanager, redirect_stdout
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any, Callable, Never, cast
|
|
@@ -174,6 +174,24 @@ def _run_command(
|
|
|
174
174
|
raw_input,
|
|
175
175
|
idempotency_key=args.idempotency_key,
|
|
176
176
|
)
|
|
177
|
+
execution = response.get("execution")
|
|
178
|
+
if not isinstance(execution, Mapping):
|
|
179
|
+
return _failure(
|
|
180
|
+
"function_protocol_error",
|
|
181
|
+
message="Lumera returned an invalid Function execution",
|
|
182
|
+
retryable=False,
|
|
183
|
+
)
|
|
184
|
+
status = str(execution.get("status", ""))
|
|
185
|
+
if status != "succeeded":
|
|
186
|
+
return _failure(
|
|
187
|
+
str(execution.get("error_code") or "function_not_succeeded"),
|
|
188
|
+
message=str(
|
|
189
|
+
execution.get("error_message")
|
|
190
|
+
or f"Function execution ended as {status or 'unknown'}"
|
|
191
|
+
),
|
|
192
|
+
retryable=False,
|
|
193
|
+
execution=dict(execution),
|
|
194
|
+
)
|
|
177
195
|
return _success("invoke", function_id=args.function_id, **response)
|
|
178
196
|
definition = registry.get(args.function_id)
|
|
179
197
|
with LocalFunctionRuntime(registry) as runtime:
|
|
@@ -29,7 +29,6 @@ from ._context import (
|
|
|
29
29
|
)
|
|
30
30
|
from ._contract import FunctionDefinition
|
|
31
31
|
from ._errors import (
|
|
32
|
-
DurableTransportUnavailableError,
|
|
33
32
|
FunctionCallModeError,
|
|
34
33
|
FunctionCancelledError,
|
|
35
34
|
FunctionCycleError,
|
|
@@ -40,9 +39,10 @@ from ._errors import (
|
|
|
40
39
|
FunctionNotFoundError,
|
|
41
40
|
FunctionRuntimeClosedError,
|
|
42
41
|
FunctionsError,
|
|
42
|
+
FunctionTransportUnavailableError,
|
|
43
43
|
UnexpectedFunctionError,
|
|
44
44
|
)
|
|
45
|
-
from ._execution import
|
|
45
|
+
from ._execution import ExecutionHandle, FunctionTransport
|
|
46
46
|
from ._registry import FunctionRegistry
|
|
47
47
|
|
|
48
48
|
ResultT = TypeVar("ResultT")
|
|
@@ -80,15 +80,14 @@ class _InvocationSyncLane:
|
|
|
80
80
|
executor can therefore fill every worker with parents that are waiting for
|
|
81
81
|
children queued behind them. Each root invocation instead owns this lane,
|
|
82
82
|
which starts a daemon thread for every active sync call. The call-stack
|
|
83
|
-
depth guard still bounds recursive nesting
|
|
84
|
-
|
|
85
|
-
workload-isolation boundary.
|
|
83
|
+
depth guard still bounds recursive nesting. The resident Function RPC child
|
|
84
|
+
process provides the production hard-termination boundary.
|
|
86
85
|
|
|
87
86
|
Python cannot safely terminate a running thread. A deadline or task
|
|
88
87
|
cancellation stops waiting for sync code but the thread may continue
|
|
89
|
-
cooperatively until the callable returns.
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
cooperatively until the callable returns. The RPC host kills the child
|
|
89
|
+
process group at the execution deadline so timed-out sync code cannot keep
|
|
90
|
+
producing side effects in the App Lambda environment.
|
|
92
91
|
"""
|
|
93
92
|
|
|
94
93
|
def __init__(self) -> None:
|
|
@@ -159,7 +158,7 @@ class LocalFunctionRuntime:
|
|
|
159
158
|
self,
|
|
160
159
|
registry: FunctionRegistry,
|
|
161
160
|
*,
|
|
162
|
-
transport:
|
|
161
|
+
transport: FunctionTransport | None = None,
|
|
163
162
|
capability_resolver: CapabilityResolver | None = None,
|
|
164
163
|
max_depth: int = 16,
|
|
165
164
|
) -> None:
|
|
@@ -557,7 +556,7 @@ class LocalFunctionRuntime:
|
|
|
557
556
|
raise FunctionInvocationError(
|
|
558
557
|
"FunctionContext must declare its trusted runtime_mode"
|
|
559
558
|
) from exc
|
|
560
|
-
if mode not in {"local", "api", "
|
|
559
|
+
if mode not in {"local", "api", "external"}:
|
|
561
560
|
raise FunctionInvocationError(f"unsupported FunctionContext runtime_mode: {mode!r}")
|
|
562
561
|
return cast(RuntimeMode, mode)
|
|
563
562
|
|
|
@@ -662,13 +661,12 @@ class _BoundFunctionCaller:
|
|
|
662
661
|
) -> ExecutionHandle[Any]:
|
|
663
662
|
transport = self._runtime.transport
|
|
664
663
|
if transport is None:
|
|
665
|
-
raise
|
|
666
|
-
"
|
|
667
|
-
"configure a test transport only for contract tests"
|
|
664
|
+
raise FunctionTransportUnavailableError(
|
|
665
|
+
"remote Function execution is unavailable in this context"
|
|
668
666
|
)
|
|
669
667
|
normalized_ref = function_ref.strip()
|
|
670
668
|
if not normalized_ref:
|
|
671
|
-
raise ValueError("
|
|
669
|
+
raise ValueError("Function reference cannot be empty")
|
|
672
670
|
normalized_key = idempotency_key.strip() if idempotency_key is not None else None
|
|
673
671
|
if idempotency_key is not None and not normalized_key:
|
|
674
672
|
raise ValueError("idempotency_key cannot be empty")
|
|
@@ -684,7 +682,8 @@ class _BoundFunctionCaller:
|
|
|
684
682
|
function_ref=normalized_ref,
|
|
685
683
|
initial_status=started.status,
|
|
686
684
|
transport=transport,
|
|
687
|
-
|
|
685
|
+
initial_result=started.result,
|
|
686
|
+
initial_error=started.error,
|
|
688
687
|
result_validator=result_validator,
|
|
689
688
|
)
|
|
690
689
|
|
|
@@ -708,7 +707,7 @@ class _BoundFunctionCaller:
|
|
|
708
707
|
)
|
|
709
708
|
decoded = json.loads(encoded)
|
|
710
709
|
if not isinstance(decoded, dict):
|
|
711
|
-
raise ValueError("
|
|
710
|
+
raise ValueError("Function input must be a JSON object") from None
|
|
712
711
|
return decoded, None
|
|
713
712
|
return definition.serialize_input(input), definition.validate_serialized_output
|
|
714
713
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Test helpers for Function contexts and
|
|
1
|
+
"""Test helpers for Function contexts and remote-transport contracts."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -25,7 +25,7 @@ __all__ = [
|
|
|
25
25
|
"ActorIdentity",
|
|
26
26
|
"ActorTag",
|
|
27
27
|
"AgentIdentity",
|
|
28
|
-
"
|
|
28
|
+
"FakeFunctionTransport",
|
|
29
29
|
"FakeStart",
|
|
30
30
|
"LocalCancellationToken",
|
|
31
31
|
"LocalScopedLumeraClient",
|
|
@@ -38,7 +38,7 @@ __all__ = [
|
|
|
38
38
|
|
|
39
39
|
@dataclass(frozen=True)
|
|
40
40
|
class FakeStart:
|
|
41
|
-
"""Recorded invocation accepted by
|
|
41
|
+
"""Recorded invocation accepted by FakeFunctionTransport."""
|
|
42
42
|
|
|
43
43
|
execution_id: str
|
|
44
44
|
function_ref: str
|
|
@@ -48,7 +48,7 @@ class FakeStart:
|
|
|
48
48
|
caller_project_id: str
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
class
|
|
51
|
+
class FakeFunctionTransport:
|
|
52
52
|
"""In-memory transport for testing start/handle behavior only."""
|
|
53
53
|
|
|
54
54
|
def __init__(self) -> None:
|
|
@@ -77,7 +77,7 @@ class FakeDurableTransport:
|
|
|
77
77
|
)
|
|
78
78
|
self._statuses[execution_id] = ExecutionStatus(
|
|
79
79
|
execution_id=execution_id,
|
|
80
|
-
status="
|
|
80
|
+
status="running",
|
|
81
81
|
)
|
|
82
82
|
self._events[execution_id] = asyncio.Event()
|
|
83
83
|
return StartedExecution(execution_id=execution_id)
|
|
@@ -44,9 +44,8 @@ _LAZY_MODULES = {
|
|
|
44
44
|
"_archive": "._archive",
|
|
45
45
|
"_builder": "._builder",
|
|
46
46
|
"_contracts": "._contracts",
|
|
47
|
-
"
|
|
47
|
+
"_rpc_event": "._rpc_event",
|
|
48
48
|
"_runtime": "._runtime",
|
|
49
|
-
"_worker_child": "._worker_child",
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
_LAZY_ATTRIBUTES = {
|
|
@@ -60,7 +60,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
60
60
|
|
|
61
61
|
contract = commands.add_parser("contract-test")
|
|
62
62
|
contract.add_argument("archive")
|
|
63
|
-
contract.add_argument("--mode", choices=("api", "
|
|
63
|
+
contract.add_argument("--mode", choices=("api", "function", "all"), default="all")
|
|
64
64
|
contract.add_argument("--expected-digest")
|
|
65
65
|
|
|
66
66
|
deploy = commands.add_parser("deploy")
|
|
@@ -173,7 +173,7 @@ def _declared_contract_modes(manifest: dict[str, object]) -> tuple[RuntimeMode,
|
|
|
173
173
|
modes.append("api")
|
|
174
174
|
function_modules = entrypoints.get("function_modules")
|
|
175
175
|
if isinstance(function_modules, list) and function_modules:
|
|
176
|
-
modes.append("
|
|
176
|
+
modes.append("function")
|
|
177
177
|
if not modes:
|
|
178
178
|
raise ValueError("release has no contract-testable entrypoints")
|
|
179
179
|
return tuple(modes)
|