gpframe 0.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.
gpframe/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ """
2
+ Unified routine execution framework with frames, events, and contexts.
3
+
4
+ gpframe wraps user-defined routines with a *frame* that provides lifecycle
5
+ management, event handling, re-execution, and shared state. The routine itself
6
+ can remain a simple function or async function, while the frame coordinates
7
+ control flow and consistency.
8
+
9
+ Key features:
10
+ - Works generically, from simple processes to event-driven extensions
11
+ and parallel execution control
12
+ - Supports both synchronous and asynchronous routines
13
+ - Synchronous routines can optionally be executed in subprocesses
14
+ (asynchronous ones are excluded)
15
+
16
+ Execution flow:
17
+ - on_start → routine execution → on_end → redo check (continue or finish)
18
+ - on_cancel is called when canceled
19
+ - on_close is always called upon termination, followed by the terminated callback
20
+
21
+ Data and error handling:
22
+ - Results, errors, and message exchange are consolidated into an ``Outcome``
23
+ - Exceptions from routines, frames, and event handlers are distinguished,
24
+ and which to re-raise is controlled by ``Raises``
25
+ - Shared data (environment, requests, event messages) is managed with
26
+ synchronized maps to ensure consistency across contexts
27
+ - Contexts (``ControllerContext``, ``EventContext``, ``RoutineContext``)
28
+ provide safe and structured access during execution
29
+
30
+ Main components:
31
+ - **Frame** — entry point that wraps and manages routines
32
+ - **ControllerContext / EventContext / RoutineContext** — execution contexts
33
+ - **Outcome** — holds results, errors, requests, and messages
34
+ - **Raises** — selects which exception types to re-raise
35
+ - **Error types** — ``FrameError``, ``TerminatedError``, ``NotTerminatedError``, etc.
36
+
37
+ gpframe provides unified control features without burdening the routine itself.
38
+ """
39
+ from .api.gpframe import Frame
40
+
41
+ from .api.gpframe import FrameError, FrameType, TerminatedError, NotTerminatedError
42
+ from .api.error import Raises
43
+
44
+ from .api.contexts import Controller
45
+ from .api.contexts import EventContext
46
+ from .api.contexts import RoutineContext
47
+
48
+ from .api.outcome import Outcome
49
+
50
+ from .api.result import NO_VALUE
51
+
52
+
53
+ __all__ = ("Frame", "FrameType",
54
+ "FrameError", "TerminatedError", "NotTerminatedError", "Raises",
55
+ "Controller", "EventContext", "RoutineContext", "Outcome",
56
+ "NO_VALUE")
57
+
File without changes
@@ -0,0 +1,220 @@
1
+ """
2
+ contexts.py
3
+
4
+ Defines abstract context interfaces for controllers, event handlers,
5
+ and routines. These contexts provide structured access to logging,
6
+ shared state, and communication channels, making it clear which
7
+ elements are read-only and which are writable.
8
+
9
+ Design notes:
10
+ - All contexts separate read-only and writable elements.
11
+ - Logging is always available.
12
+ - Shared state and message access are structured to prevent
13
+ unintended interference.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from abc import ABC, abstractmethod
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from logging import Logger
22
+
23
+ from ..impl.result import RoutineResult
24
+ from ..impl.syncm import SynchronizedMapReader, SynchronizedMapUpdater
25
+
26
+
27
+ class Controller(ABC):
28
+ """Controller for running routine.
29
+
30
+ Provides:
31
+ - Read-only access to environment and inbound messages
32
+ - Writable request channel
33
+ - Routine result access
34
+ - Lifecycle control (stop / is_running)
35
+
36
+ Notes:
37
+ - `environment`, `event_msg`, `routine_msg`, `routine_result` are read-only
38
+ - `request` is the only writable channel
39
+ - `stop()` is idempotent
40
+ """
41
+ __slots__ = ()
42
+ @property
43
+ @abstractmethod
44
+ def frame_name(self) -> str:
45
+ ...
46
+ @property
47
+ @abstractmethod
48
+ def logger(self) -> Logger:
49
+ ...
50
+ @property
51
+ @abstractmethod
52
+ def routine_in_subprocess(self) -> bool:
53
+ ...
54
+ @property
55
+ @abstractmethod
56
+ def environment(self) -> SynchronizedMapReader:
57
+ ...
58
+ @property
59
+ @abstractmethod
60
+ def request(self) -> SynchronizedMapUpdater:
61
+ """Writable channel to send requests to the routine.
62
+
63
+ This is a best-effort communication mechanism:
64
+ - The routine may use these requests as hints or instructions.
65
+ - There is no guarantee that any given request will be executed
66
+ or even recognized.
67
+ - Actual behavior depends on the routine's implementation.
68
+
69
+ Use this as a way to suggest actions, not as a strict control channel.
70
+ """
71
+ @property
72
+ @abstractmethod
73
+ def event_msg(self) -> SynchronizedMapReader:
74
+ ...
75
+ @property
76
+ @abstractmethod
77
+ def routine_msg(self) -> SynchronizedMapReader:
78
+ ...
79
+ @property
80
+ @abstractmethod
81
+ def routine_result(self) -> RoutineResult:
82
+ """Result container for the routine.
83
+
84
+ Notes:
85
+ - The `RoutineResult.value` field is initialized to `NO_VALUE`.
86
+ - This remains `NO_VALUE` if the routine has not yet executed,
87
+ or if it finished without producing a result.
88
+ - Compare against `NO_VALUE` to detect these cases.
89
+ """
90
+ @abstractmethod
91
+ def stop(self, *, kill: bool = False) -> None:
92
+ """Request the routine to stop.
93
+
94
+ Behavior depends on how the routine is executed:
95
+ - Async routine: calls `cancel()`. If `kill=True`, the flag is ignored.
96
+ - Subprocess routine: calls `terminate()`. If `kill=True`, calls `kill()` instead.
97
+ - Synchronous in-process routine: not supported → raises TypeError.
98
+
99
+ Args:
100
+ kill: If True and the routine is a subprocess, use a hard kill instead
101
+ of graceful termination. Ignored for async routines.
102
+
103
+ Raises:
104
+ TypeError: If the routine is a synchronous in-process function.
105
+ """
106
+ @property
107
+ @abstractmethod
108
+ def routine_is_running(self) -> bool:
109
+ """Whether the routine appears to be running.
110
+
111
+ Notes:
112
+ - This value is an approximate indicator only.
113
+ - It may not reflect the exact, real-time execution state.
114
+ - Use it as a guideline rather than a strict guarantee.
115
+ """
116
+
117
+
118
+
119
+ class EventContext(ABC):
120
+ """Context used inside event handlers.
121
+
122
+ Provides:
123
+ - Read-only access to environment, request, routine messages, and results
124
+ - Writable event message channel
125
+
126
+ Notes:
127
+ - `request` is read-only here (no sending)
128
+ - `event_msg` is the only writable channel
129
+ """
130
+ __slots__ = ()
131
+ @property
132
+ @abstractmethod
133
+ def frame_name(self) -> str:
134
+ ...
135
+ @property
136
+ @abstractmethod
137
+ def logger(self) -> Logger:
138
+ ...
139
+ @property
140
+ @abstractmethod
141
+ def routine_in_subprocess(self) -> bool:
142
+ ...
143
+ @property
144
+ @abstractmethod
145
+ def environment(self) -> SynchronizedMapReader:
146
+ ...
147
+ @property
148
+ @abstractmethod
149
+ def request(self) -> SynchronizedMapReader:
150
+ ...
151
+ @property
152
+ @abstractmethod
153
+ def event_msg(self) -> SynchronizedMapUpdater:
154
+ ...
155
+ @property
156
+ @abstractmethod
157
+ def routine_msg(self) -> SynchronizedMapReader:
158
+ ...
159
+ @property
160
+ @abstractmethod
161
+ def routine_result(self) -> RoutineResult:
162
+ """Result container for the routine.
163
+
164
+ Notes:
165
+ - The `RoutineResult.value` field is initialized to `NO_VALUE`.
166
+ - This remains `NO_VALUE` if the routine has not yet executed,
167
+ or if it finished without producing a result.
168
+ - Compare against `NO_VALUE` to detect these cases.
169
+ """
170
+
171
+
172
+ class RoutineContext(ABC):
173
+ """Context for routines executed by the system.
174
+
175
+ Provides:
176
+ - Read-only access to environment, request, and event messages
177
+ - Writable routine message channel
178
+ - Logger name and subprocess execution flag
179
+
180
+ Notes:
181
+ - When `executes_as_subprocess` is True, data must be serializable
182
+ - `routine_msg` is the only writable channel
183
+ """
184
+ __slots__ = ()
185
+ @property
186
+ @abstractmethod
187
+ def frame_name(self) -> str:
188
+ ...
189
+ @property
190
+ @abstractmethod
191
+ def logger_name(self) -> str:
192
+ """Name of the logger channel associated with this routine.
193
+
194
+ Unlike other contexts, a `Logger` instance is not provided directly.
195
+ Instead, use this name to obtain a logger when needed.
196
+
197
+ Reason:
198
+ - Supports routines that may run in a separate subprocess,
199
+ where direct logger objects cannot be shared safely.
200
+ """
201
+ @property
202
+ @abstractmethod
203
+ def routine_in_subprocess(self) -> bool:
204
+ ...
205
+ @property
206
+ @abstractmethod
207
+ def environment(self) -> SynchronizedMapReader:
208
+ ...
209
+ @property
210
+ @abstractmethod
211
+ def request(self) -> SynchronizedMapUpdater:
212
+ ...
213
+ @property
214
+ @abstractmethod
215
+ def event_msg(self) -> SynchronizedMapReader:
216
+ ...
217
+ @property
218
+ @abstractmethod
219
+ def routine_msg(self) -> SynchronizedMapReader:
220
+ ...
gpframe/api/error.py ADDED
@@ -0,0 +1,52 @@
1
+ """
2
+ Exception handling policy for gpframe.
3
+
4
+ Defines the ``Raises`` enumeration, an ``IntFlag`` used to
5
+ specify which categories of exceptions should be re-raised.
6
+
7
+ Notes:
8
+ - Members can be combined with bitwise operations.
9
+ - ``Raises.ALL`` is equivalent to combining all flags.
10
+ """
11
+
12
+ from enum import IntFlag, auto
13
+
14
+ class Raises(IntFlag):
15
+ """
16
+ Enumeration of flags that control which exceptions are re-raised.
17
+
18
+ Members:
19
+ SUPPRESS
20
+ Suppress all exceptions.
21
+ ROUTINE
22
+ Re-raise exceptions raised in routines.
23
+ FRAME
24
+ Re-raise exceptions raised during frame control.
25
+ HANDLER
26
+ Re-raise exceptions raised in handlers.
27
+ ALL
28
+ Equivalent to combining ROUTINE, FRAME, and HANDLER.
29
+ """
30
+ SUPPRESS = 0
31
+ ROUTINE = auto()
32
+ FRAME = auto()
33
+ HANDLER = auto()
34
+ ALL = ROUTINE | FRAME | HANDLER
35
+
36
+ def matches(self, r: Exception | None, f: Exception | None, h: Exception | None) -> bool:
37
+ """
38
+ Check whether any of the given exceptions should be re-raised.
39
+
40
+ Args:
41
+ r: Exception from a routine, or None if none occurred.
42
+ f: Exception from frame control, or None if none occurred.
43
+ h: Exception from a handler, or None if none occurred.
44
+
45
+ Returns:
46
+ True if at least one exception matches this Raises setting,
47
+ otherwise False.
48
+ """
49
+ result = r is not None and self & Raises.ROUTINE
50
+ result |= f is not None and self & Raises.FRAME
51
+ result |= h is not None and self & Raises.HANDLER
52
+ return bool(result)
gpframe/api/gpframe.py ADDED
@@ -0,0 +1,330 @@
1
+ """
2
+ gpframe protocol and core error types.
3
+
4
+ This module defines the ``Frame`` protocol and the abstract base class
5
+ ``FrameType``, which represent the entry point for wrapping and managing
6
+ routines inside a frame. These provide configuration methods, lifecycle
7
+ management, and access to controller contexts.
8
+
9
+ It also defines core error types:
10
+ - ``FrameError`` — groups exceptions from routines, frame control,
11
+ and event handlers
12
+ - ``TerminatedError`` — raised when an operation requires an active
13
+ frame but it has already terminated
14
+ - ``NotTerminatedError`` — raised when termination was expected but
15
+ the frame has not yet terminated
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from abc import ABC, abstractmethod
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ import asyncio
24
+ import logging
25
+
26
+ from .error import Raises
27
+ from ..impl.context.controller import Controller
28
+ from ..impl.frame import Routine
29
+ from ..impl.handler.event import EventHandler
30
+ from ..impl.handler.redo import RedoHandler
31
+ from ..impl.handler.exception import ExceptionHandler
32
+ from ..impl.handler.terminated import TerminatedHandler
33
+
34
+ from .outcome import Outcome
35
+
36
+
37
+
38
+ def Frame(routine: Routine):
39
+ """Create a frame that manages execution of the given routine.
40
+
41
+ ``Frame`` is exposed as a factory function (not a class), but the
42
+ returned object behaves like a frame instance with the following features:
43
+
44
+ - Executes the routine (sync, async, or subprocess-backed)
45
+ - Manages lifecycle: start, cancel, close, redo
46
+ - Provides access to contexts:
47
+ * ControllerContext
48
+ * EventContext
49
+ * RoutineContext
50
+ - Consolidates results, errors, and messages into an ``Outcome``
51
+ - Raises errors according to the configured ``Raises`` policy
52
+
53
+ Notes:
54
+ - Use ``FrameType`` for type annotations and static type checking.
55
+ - This function simply constructs and initializes the underlying frame.
56
+ """
57
+ from ..impl.frame import create_frame_role
58
+ role = create_frame_role(routine)
59
+ role.core.initialize()
60
+ return role.interface
61
+
62
+
63
+ class FrameType(ABC):
64
+ __slots__ = ()
65
+ @abstractmethod
66
+ def set_name(self, name: str) -> None:
67
+ """Set a display name for the frame instance."""
68
+
69
+ @abstractmethod
70
+ def set_logger(self, logger: logging.Logger):
71
+ """Set the logger used by the frame.
72
+
73
+ By default, a logger named ``"gpframe"`` is used.
74
+ """
75
+
76
+ @abstractmethod
77
+ def set_raises(self, raises: Raises):
78
+ """Configure which categories of exceptions should be re-raised.
79
+
80
+ The ``raises`` flag determines how exceptions are propagated out of
81
+ the frame. Possible values are defined in the ``Raises`` enumeration:
82
+
83
+ - ``Raises.SUPPRESS`` — suppress all exceptions
84
+ - ``Raises.ROUTINE`` — re-raise exceptions from the routine
85
+ - ``Raises.FRAME`` — re-raise exceptions from frame control
86
+ - ``Raises.HANDLER`` — re-raise exceptions from event handlers
87
+ - ``Raises.ALL`` — re-raise all of the above
88
+
89
+ Notes:
90
+ - Flags can be combined with bitwise operations.
91
+ - ``Raises.ALL`` is equivalent to ``ROUTINE | FRAME | HANDLER``.
92
+ - ``asyncio.CancelledError`` is not included; instead, the
93
+ ``on_cancel`` handler is invoked when cancellation occurs.
94
+ """
95
+
96
+ @abstractmethod
97
+ def set_environments(self, environments: dict):
98
+ """Set the initial environment values for the frame.
99
+
100
+ These values populate the shared read-only environment accessible
101
+ from contexts. No interface is provided to modify the environment
102
+ after initialization.
103
+
104
+ Notes:
105
+ - Internally, a shallow copy of the given mapping is used.
106
+ """
107
+
108
+ @abstractmethod
109
+ def set_requests(self, requests: dict):
110
+ """Set the initial request values for the routine.
111
+
112
+ These act as the starting instructions provided to the routine.
113
+ Whether or not the routine honors these requests depends on its
114
+ implementation.
115
+
116
+ Notes:
117
+ - Internally, a shallow copy of the given mapping is used.
118
+ """
119
+
120
+ @abstractmethod
121
+ def start(self, *, as_subprocess: bool = False) -> tuple[Controller, asyncio.Task[None]]:
122
+ """Start the frame and return its controller context and task.
123
+
124
+ The frame is launched as an ``asyncio.Task`` that manages the lifecycle
125
+ of the routine. A tuple is returned containing:
126
+
127
+ - ``ControllerContext`` — interface for interacting with the running frame
128
+ - ``asyncio.Task`` — the task object representing the running frame
129
+
130
+ Args:
131
+ as_subprocess: If True, run the routine in a separate subprocess.
132
+ This option is only supported for synchronous routines.
133
+ If used with an asynchronous routine, a ``TypeError`` is raised.
134
+
135
+ Returns:
136
+ A tuple ``(context, task)`` where ``context`` is a ``ControllerContext``
137
+ and ``task`` is the ``asyncio.Task`` running the frame.
138
+ """
139
+
140
+ @abstractmethod
141
+ def set_terminated_callback(self, handler: TerminatedHandler):
142
+ """Register a callback to be invoked after the frame has fully closed.
143
+
144
+ The given handler is called once termination and cleanup are complete.
145
+ Unlike calling ``FrameType.get_outcome()`` directly, the handler is
146
+ guaranteed to receive a valid ``Outcome`` object.
147
+
148
+ Args:
149
+ handler: A callable that takes the final ``Outcome`` of the routine.
150
+ """
151
+
152
+ @abstractmethod
153
+ def set_exception_handler(self, handler: ExceptionHandler):
154
+ """Register a handler to be called whenever an exception occurs.
155
+
156
+ The handler is invoked for each exception raised inside the frame,
157
+ including ``asyncio.CancelledError``.
158
+
159
+ Behavior:
160
+ - If the handler completes normally (does not raise), the exception
161
+ is treated as consumed and the frame continues running.
162
+ - If the handler re-raises the exception (or raises another one),
163
+ normal exception processing resumes and the frame may stop.
164
+
165
+ Args:
166
+ handler: A callable that takes an exception instance. It should
167
+ either handle the exception silently or re-raise it to let
168
+ the frame propagate the error.
169
+ """
170
+
171
+ @abstractmethod
172
+ def set_redo_handler(self, handler: RedoHandler) -> None:
173
+ """Register a handler to decide whether the routine should be re-executed.
174
+
175
+ The handler is invoked after ``on_end`` completes. If the handler returns
176
+ ``True``, the routine will be executed again. If it returns ``False``,
177
+ execution finishes normally.
178
+
179
+ Notes:
180
+ - If the handler does not return a boolean value, the result is treated
181
+ as ``False``.
182
+ - The handler may be called multiple times if the routine is repeatedly
183
+ re-executed.
184
+ """
185
+
186
+ @abstractmethod
187
+ def set_on_open(self, handler: EventHandler) -> None:
188
+ """Register a handler to be called once at the beginning of the frame.
189
+
190
+ The open handler is invoked a single time when the frame lifecycle starts,
191
+ before any routine execution begins.
192
+
193
+ Notes:
194
+ - Called only once per frame.
195
+ - Use this for initialization that must occur before the first routine run.
196
+ """
197
+
198
+ @abstractmethod
199
+ def set_on_start(self, handler: EventHandler) -> None:
200
+ """Register a handler to be called immediately before the routine starts.
201
+
202
+ The handler is invoked each time the routine is about to execute,
203
+ including repeated executions triggered by a redo handler.
204
+ """
205
+
206
+ @abstractmethod
207
+ def set_on_end(self, handler: EventHandler) -> None:
208
+ """Register a handler to be called after the routine finishes.
209
+
210
+ The handler is invoked immediately after the routine completes,
211
+ regardless of whether it ended normally or with an error.
212
+
213
+ Notes:
214
+ - If the routine is executed multiple times (e.g. due to a redo
215
+ handler), this end handler is called after each execution.
216
+ - After the end handler returns, the redo handler (if set) is
217
+ invoked to decide whether the routine should be run again.
218
+ """
219
+
220
+ @abstractmethod
221
+ def set_on_cancel(self, handler: EventHandler) -> None:
222
+ """Register a handler to be called when the routine is cancelled.
223
+
224
+ The handler is invoked if an ``asyncio.CancelledError`` is raised.
225
+ It is executed under ``asyncio.shield`` so that cancellation does not
226
+ propagate into the handler itself.
227
+
228
+ After the cancel handler completes, the ``on_close`` handler is invoked
229
+ as part of the normal termination sequence.
230
+ """
231
+
232
+ @abstractmethod
233
+ def set_on_close(self, handler: EventHandler) -> None:
234
+ """Register a handler to be called during frame shutdown.
235
+
236
+ The close handler is always invoked as part of the shutdown sequence,
237
+ regardless of whether the routine finished normally, was cancelled,
238
+ or raised an error. It is executed under ``asyncio.shield`` so that
239
+ cancellation does not propagate into the handler itself.
240
+ """
241
+
242
+ @abstractmethod
243
+ def get_outcome(self) -> Outcome:
244
+ """Return the final outcome of the frame execution.
245
+
246
+ The outcome is only available once the frame has fully terminated.
247
+ If called before termination, a ``NotTerminatedError`` is raised.
248
+
249
+ Returns:
250
+ Outcome: The result container holding the routine's return value,
251
+ errors, requests, and message exchange.
252
+
253
+ Notes:
254
+ - To obtain the outcome reliably, prefer registering a
255
+ terminated callback via ``set_terminated_callback``.
256
+ - If the routine has not produced a value, the return field
257
+ will be set to ``NO_VALUE``.
258
+ """
259
+
260
+ @abstractmethod
261
+ def peek_outcome(self) -> Outcome | None:
262
+ """Return the outcome if available, or None if the frame is still running.
263
+
264
+ Unlike ``get_outcome``, this method never raises ``NotTerminatedError``.
265
+ It simply returns ``None`` when the frame has not yet terminated.
266
+
267
+ Returns:
268
+ Outcome | None: The final outcome if the frame has finished,
269
+ otherwise ``None``.
270
+
271
+ Notes:
272
+ - This method is useful for polling without exceptions.
273
+ - To obtain the outcome reliably, prefer registering a
274
+ terminated callback via ``set_terminated_callback``.
275
+ """
276
+
277
+ class FrameError(Exception):
278
+ """Exception raised when errors occur inside a frame.
279
+
280
+ This aggregates exceptions from three possible sources:
281
+ - ``routine_error`` — error raised inside the user routine
282
+ - ``frame_error`` — error raised during frame control
283
+ - ``handler_error`` — error raised in an event handler
284
+
285
+ Attributes:
286
+ routine_error (Exception | None): The routine error, if any.
287
+ frame_error (Exception | None): The frame control error, if any.
288
+ handler_error (Exception | None): The last handler error that occurred,
289
+ or None if none occurred. If multiple handler errors arise,
290
+ only the most recent one is stored.
291
+
292
+ Notes:
293
+ - Each attribute may be ``None`` if no error of that type occurred.
294
+ - The string representation includes the type names of the collected errors.
295
+ - ``asyncio.CancelledError`` is **not** included here; instead, the
296
+ ``on_cancel`` handler is invoked when cancellation occurs.
297
+ """
298
+ def __init__(
299
+ self,
300
+ routine_error: Exception | None,
301
+ frame_error: Exception | None,
302
+ handler_error: Exception | None
303
+ ):
304
+ super().__init__(
305
+ "Exceptions raised in the frame. "
306
+ f"routine_error = {type(routine_error).__name__ if routine_error else "None"}"
307
+ f"frame_error = {type(frame_error).__name__ if frame_error else "None"}, "
308
+ f"handler_error = {type(handler_error).__name__ if handler_error else "None"}"
309
+ )
310
+ self.routine_error = routine_error
311
+ self.frame_error = frame_error
312
+ self.handler_error = handler_error
313
+
314
+ class TerminatedError(Exception):
315
+ """Raised when accessing a context resource after frame termination.
316
+
317
+ This error is thrown if a ``SynchronizedMapReader``/``SynchronizedMapUpdater``
318
+ (or similar context-managed resource) is accessed after the frame has
319
+ already terminated. It prevents use of stale or invalid state once
320
+ the frame lifecycle has ended.
321
+ """
322
+
323
+ class NotTerminatedError(Exception):
324
+ """Raised when requesting an outcome before frame termination.
325
+
326
+ This error occurs if ``FrameType.get_outcome()`` is called while the
327
+ frame is still running. An outcome is only available once the frame has
328
+ fully terminated.
329
+ """
330
+