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 +57 -0
- gpframe/api/__init__.py +0 -0
- gpframe/api/contexts.py +220 -0
- gpframe/api/error.py +52 -0
- gpframe/api/gpframe.py +330 -0
- gpframe/api/outcome.py +59 -0
- gpframe/api/result.py +23 -0
- gpframe/impl/__init__.py +0 -0
- gpframe/impl/context/__init__.py +0 -0
- gpframe/impl/context/controller.py +59 -0
- gpframe/impl/context/event.py +50 -0
- gpframe/impl/context/routine.py +55 -0
- gpframe/impl/frame.py +579 -0
- gpframe/impl/handler/__init__.py +0 -0
- gpframe/impl/handler/error.py +6 -0
- gpframe/impl/handler/event.py +40 -0
- gpframe/impl/handler/exception.py +53 -0
- gpframe/impl/handler/redo.py +41 -0
- gpframe/impl/handler/terminated.py +44 -0
- gpframe/impl/outcome.py +67 -0
- gpframe/impl/result.py +69 -0
- gpframe/impl/state.py +173 -0
- gpframe/impl/stop.py +137 -0
- gpframe/impl/syncm.py +141 -0
- gpframe-0.0.1.dist-info/METADATA +48 -0
- gpframe-0.0.1.dist-info/RECORD +29 -0
- gpframe-0.0.1.dist-info/WHEEL +5 -0
- gpframe-0.0.1.dist-info/licenses/LICENSE +21 -0
- gpframe-0.0.1.dist-info/top_level.txt +1 -0
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
|
+
|
gpframe/api/__init__.py
ADDED
|
File without changes
|
gpframe/api/contexts.py
ADDED
|
@@ -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
|
+
|