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.
Files changed (81) hide show
  1. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/PKG-INFO +1 -1
  2. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/function_resources.py +4 -25
  3. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/__init__.py +8 -9
  4. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_artifacts.py +1 -1
  5. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_context.py +3 -3
  6. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_errors.py +4 -8
  7. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_execution.py +30 -25
  8. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_remote.py +59 -32
  9. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_runner.py +19 -1
  10. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_runtime.py +15 -16
  11. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/testing.py +5 -5
  12. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/__init__.py +1 -2
  13. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/__main__.py +2 -2
  14. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_api_child.py +68 -26
  15. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_projection.py +7 -2
  16. lumera-0.41.3.dev2026091001/lumera/releases/_rpc_event.py +233 -0
  17. lumera-0.41.3.dev2026091001/lumera/releases/_rpc_process.py +266 -0
  18. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_runtime.py +2 -2
  19. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_runtime_child.py +1 -1
  20. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/PKG-INFO +1 -1
  21. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/SOURCES.txt +2 -4
  22. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/pyproject.toml +1 -1
  23. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_function_resources.py +3 -23
  24. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_lazy_imports.py +0 -23
  25. lumera-0.41.2.dev20260909/lumera/releases/_durable_event.py +0 -378
  26. lumera-0.41.2.dev20260909/lumera/releases/_function_child.py +0 -217
  27. lumera-0.41.2.dev20260909/lumera/releases/_worker_child.py +0 -459
  28. lumera-0.41.2.dev20260909/lumera/releases/_worker_protocol.py +0 -131
  29. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/__init__.py +0 -0
  30. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/_live.py +0 -0
  31. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/_utils.py +0 -0
  32. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/agents.py +0 -0
  33. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/automations.py +0 -0
  34. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/email.py +0 -0
  35. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/exceptions.py +0 -0
  36. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/files.py +0 -0
  37. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/flags.py +0 -0
  38. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/__main__.py +0 -0
  39. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_config.py +0 -0
  40. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_contract.py +0 -0
  41. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_dependencies.py +0 -0
  42. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_jsonschema.py +0 -0
  43. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/functions/_registry.py +0 -0
  44. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/google.py +0 -0
  45. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/__init__.py +0 -0
  46. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/bill.py +0 -0
  47. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/finch.py +0 -0
  48. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/google.py +0 -0
  49. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/kafka.py +0 -0
  50. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/netsuite.py +0 -0
  51. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/salesforce.py +0 -0
  52. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/slack.py +0 -0
  53. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/integrations/snowflake.py +0 -0
  54. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/llm.py +0 -0
  55. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/pb.py +0 -0
  56. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_archive.py +0 -0
  57. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_builder.py +0 -0
  58. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_config.py +0 -0
  59. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_contracts.py +0 -0
  60. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_errors.py +0 -0
  61. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_schemas.py +0 -0
  62. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/releases/_target.py +0 -0
  63. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/sdk.py +0 -0
  64. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/storage.py +0 -0
  65. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera/webhooks.py +0 -0
  66. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/dependency_links.txt +0 -0
  67. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/requires.txt +0 -0
  68. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/lumera.egg-info/top_level.txt +0 -0
  69. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/setup.cfg +0 -0
  70. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_agents_live.py +0 -0
  71. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_automations.py +0 -0
  72. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_flags.py +0 -0
  73. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_bill.py +0 -0
  74. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_finch.py +0 -0
  75. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_google.py +0 -0
  76. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_kafka.py +0 -0
  77. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_netsuite.py +0 -0
  78. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_salesforce.py +0 -0
  79. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_integrations_snowflake.py +0 -0
  80. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_sdk.py +0 -0
  81. {lumera-0.41.2.dev20260909 → lumera-0.41.3.dev2026091001}/tests/test_webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumera
3
- Version: 0.41.2.dev20260909
3
+ Version: 0.41.3.dev2026091001
4
4
  Summary: SDK for building on Lumera platform
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: requests>=2.33.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
- options: dict[str, Any] = {}
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
- InvalidDurableWaitError,
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
- "InvalidDurableWaitError",
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, durable,
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", "worker", "external"]
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 durable Function composition interface."""
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", "worker", "external"}:
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 DurableTransportUnavailableError(FunctionsError):
162
- """No durable Function transport is configured in this phase/context."""
161
+ class FunctionTransportUnavailableError(FunctionsError):
162
+ """No remote Function transport is configured in this context."""
163
163
 
164
164
 
165
- class DurableWaitNotAllowedError(FunctionsError):
166
- """A Function worker attempted to wait for durable child work."""
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
- """Durable-execution interfaces reserved for later transport adapters."""
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 DurableWaitNotAllowedError, InvalidDurableWaitError
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 durable execution status snapshot."""
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
- """Identity returned when a durable transport accepts an invocation."""
28
+ """Execution snapshot returned by a Function transport."""
29
29
 
30
30
  execution_id: str
31
- status: str = "queued"
31
+ status: str = "running"
32
+ result: Any | None = None
33
+ error: Mapping[str, Any] | None = None
32
34
 
33
35
 
34
- class DurableFunctionTransport(Protocol):
35
- """Backend adapter seam; Phase 1 intentionally ships no real transport."""
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
- Waiting is deliberately bounded. Handles created inside Function-worker
60
- contexts reject ``result`` so a saturated worker pool cannot deadlock while
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: DurableFunctionTransport,
71
- wait_allowed: bool,
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._wait_allowed = wait_allowed
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 durable status without waiting for completion."""
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 InvalidDurableWaitError(
93
+ raise InvalidFunctionWaitError(
97
94
  "ExecutionHandle.result requires a positive, finite timeout"
98
95
  )
99
- result = await self._transport.get_result(
100
- self.execution_id,
101
- timeout=timeout,
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
- """Durable backend transport and remote execution handles."""
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 DurableFunctionTransport, ExecutionHandle, ExecutionStatus, StartedExecution
11
+ from ._execution import ExecutionHandle, ExecutionStatus, FunctionTransport, StartedExecution
12
12
 
13
- _TERMINAL = {"succeeded", "failed", "indeterminate", "timed_out", "cancelled"}
14
- _MAX_LONG_POLL_SECONDS = 20
13
+ _TERMINAL = {"succeeded", "failed", "indeterminate", "timed_out"}
15
14
 
16
15
 
17
- class RemoteFunctionTransport(DurableFunctionTransport):
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
- return StartedExecution(execution_id=str(execution["id"]), status=str(execution["status"]))
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
- error = None
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
- # An older backend may ignore Prefer and return immediately. Preserve
93
- # coarse one-second polling in that case instead of hammering its status
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
- wait_allowed=True,
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
- __all__ = ["RemoteFunctionTransport", "invoke_remote"]
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 DurableFunctionTransport, ExecutionHandle
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; Phase 1 is local-only and does
84
- not use this thread-per-call mechanism as a production admission-control or
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. Production adapters must execute
90
- project code behind the separately specified process boundary when hard
91
- termination is required.
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: DurableFunctionTransport | None = None,
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", "worker", "external"}:
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 DurableTransportUnavailableError(
666
- "durable Function execution is not available in Phase 1; "
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("durable Function reference cannot be empty")
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
- wait_allowed=self._context.runtime_mode != "worker",
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("durable Function input must be a JSON object") from None
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 future durable-transport contracts."""
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
- "FakeDurableTransport",
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 FakeDurableTransport."""
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 FakeDurableTransport:
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="queued",
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
- "_function_child": "._function_child",
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", "worker", "all"), default="all")
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("worker")
176
+ modes.append("function")
177
177
  if not modes:
178
178
  raise ValueError("release has no contract-testable entrypoints")
179
179
  return tuple(modes)