tapio-py 0.1.0__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.
- tapio/__init__.py +122 -0
- tapio/actor/__init__.py +90 -0
- tapio/actor/adapter.py +217 -0
- tapio/actor/ask.py +375 -0
- tapio/actor/behavior.py +719 -0
- tapio/actor/cell.py +1403 -0
- tapio/actor/context.py +264 -0
- tapio/actor/dead_letters.py +370 -0
- tapio/actor/events.py +112 -0
- tapio/actor/mailbox.py +350 -0
- tapio/actor/path.py +92 -0
- tapio/actor/ref.py +193 -0
- tapio/actor/router.py +239 -0
- tapio/actor/signals.py +82 -0
- tapio/actor/stash.py +148 -0
- tapio/actor/supervision.py +231 -0
- tapio/actor/system.py +610 -0
- tapio/actor/timers.py +260 -0
- tapio/actor/watch.py +96 -0
- tapio/dispatch/__init__.py +10 -0
- tapio/dispatch/blocking.py +174 -0
- tapio/dispatch/dispatcher.py +76 -0
- tapio/errors.py +197 -0
- tapio/logging.py +57 -0
- tapio/message.py +30 -0
- tapio/py.typed +0 -0
- tapio/remote/__init__.py +10 -0
- tapio/remote/address.py +179 -0
- tapio/remote/association.py +1008 -0
- tapio/remote/codec.py +418 -0
- tapio/remote/context.py +108 -0
- tapio/remote/endpoint.py +729 -0
- tapio/remote/failure.py +195 -0
- tapio/remote/handshake.py +310 -0
- tapio/remote/protocol.py +37 -0
- tapio/remote/ref.py +289 -0
- tapio/remote/registry.py +165 -0
- tapio/remote/spawner.py +555 -0
- tapio/remote/transport.py +489 -0
- tapio/settings.py +199 -0
- tapio/testkit/__init__.py +47 -0
- tapio/testkit/behavior.py +499 -0
- tapio/testkit/leaks.py +69 -0
- tapio/testkit/plugin.py +104 -0
- tapio/testkit/probe.py +332 -0
- tapio/testkit/remote.py +329 -0
- tapio/validation.py +155 -0
- tapio/version.py +34 -0
- tapio_py-0.1.0.dist-info/METADATA +273 -0
- tapio_py-0.1.0.dist-info/RECORD +53 -0
- tapio_py-0.1.0.dist-info/WHEEL +4 -0
- tapio_py-0.1.0.dist-info/entry_points.txt +2 -0
- tapio_py-0.1.0.dist-info/licenses/LICENSE +202 -0
tapio/__init__.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""tapio: a Pekko-inspired actor toolkit for Python.
|
|
2
|
+
|
|
3
|
+
Local, typed, asyncio-native actors with supervision, and Pydantic models
|
|
4
|
+
throughout.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tapio.actor.behavior import AbstractBehavior, Behavior, Behaviors
|
|
8
|
+
from tapio.actor.context import ActorContext
|
|
9
|
+
from tapio.actor.dead_letters import DeadLetter, DeadLetterReason
|
|
10
|
+
from tapio.actor.events import EventStream, Subscription
|
|
11
|
+
from tapio.actor.mailbox import MailboxConfig, OverflowStrategy
|
|
12
|
+
from tapio.actor.path import ActorPath
|
|
13
|
+
from tapio.actor.ref import ActorRef
|
|
14
|
+
from tapio.actor.router import RoundRobin, Routers, RoutingStrategy
|
|
15
|
+
from tapio.actor.signals import ChildFailed, PostStop, PreRestart, Signal, Terminated
|
|
16
|
+
from tapio.actor.stash import StashBuffer
|
|
17
|
+
from tapio.actor.supervision import Backoff, Decision, SupervisorStrategy
|
|
18
|
+
from tapio.actor.system import ActorSystem
|
|
19
|
+
from tapio.actor.timers import TimerScheduler
|
|
20
|
+
from tapio.errors import (
|
|
21
|
+
ActorNameError,
|
|
22
|
+
ActorSystemTerminating,
|
|
23
|
+
AskTargetTerminated,
|
|
24
|
+
AskTargetUnreachable,
|
|
25
|
+
AskTimeoutError,
|
|
26
|
+
AskTypeError,
|
|
27
|
+
BehaviorRegistrationError,
|
|
28
|
+
BehaviorTypeError,
|
|
29
|
+
FrameTooLargeError,
|
|
30
|
+
HandshakeError,
|
|
31
|
+
InsecureRemoteConfig,
|
|
32
|
+
MailboxFullError,
|
|
33
|
+
MessageDecodingError,
|
|
34
|
+
MessageEncodingError,
|
|
35
|
+
MessageRegistrationError,
|
|
36
|
+
MessageTypeError,
|
|
37
|
+
RefResolutionError,
|
|
38
|
+
StashOverflowError,
|
|
39
|
+
TapioError,
|
|
40
|
+
WatchError,
|
|
41
|
+
)
|
|
42
|
+
from tapio.message import Message
|
|
43
|
+
from tapio.remote.address import Address
|
|
44
|
+
from tapio.remote.failure import PeerUnreachable
|
|
45
|
+
from tapio.remote.registry import register_message
|
|
46
|
+
from tapio.remote.spawner import (
|
|
47
|
+
NoArgs,
|
|
48
|
+
Spawn,
|
|
49
|
+
Spawned,
|
|
50
|
+
SpawnFailed,
|
|
51
|
+
SpawnFailure,
|
|
52
|
+
SpawnReply,
|
|
53
|
+
remote_behavior,
|
|
54
|
+
spawner,
|
|
55
|
+
)
|
|
56
|
+
from tapio.settings import RemoteSettings, TapioSettings, TLSSettings
|
|
57
|
+
from tapio.version import __version__
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"AbstractBehavior",
|
|
61
|
+
"ActorContext",
|
|
62
|
+
"ActorNameError",
|
|
63
|
+
"ActorPath",
|
|
64
|
+
"ActorRef",
|
|
65
|
+
"ActorSystem",
|
|
66
|
+
"ActorSystemTerminating",
|
|
67
|
+
"Address",
|
|
68
|
+
"AskTargetTerminated",
|
|
69
|
+
"AskTargetUnreachable",
|
|
70
|
+
"AskTimeoutError",
|
|
71
|
+
"AskTypeError",
|
|
72
|
+
"Backoff",
|
|
73
|
+
"Behavior",
|
|
74
|
+
"BehaviorRegistrationError",
|
|
75
|
+
"BehaviorTypeError",
|
|
76
|
+
"Behaviors",
|
|
77
|
+
"ChildFailed",
|
|
78
|
+
"DeadLetter",
|
|
79
|
+
"DeadLetterReason",
|
|
80
|
+
"Decision",
|
|
81
|
+
"EventStream",
|
|
82
|
+
"FrameTooLargeError",
|
|
83
|
+
"HandshakeError",
|
|
84
|
+
"InsecureRemoteConfig",
|
|
85
|
+
"MailboxConfig",
|
|
86
|
+
"MailboxFullError",
|
|
87
|
+
"Message",
|
|
88
|
+
"MessageDecodingError",
|
|
89
|
+
"MessageEncodingError",
|
|
90
|
+
"MessageRegistrationError",
|
|
91
|
+
"MessageTypeError",
|
|
92
|
+
"NoArgs",
|
|
93
|
+
"OverflowStrategy",
|
|
94
|
+
"PeerUnreachable",
|
|
95
|
+
"PostStop",
|
|
96
|
+
"PreRestart",
|
|
97
|
+
"RefResolutionError",
|
|
98
|
+
"RemoteSettings",
|
|
99
|
+
"RoundRobin",
|
|
100
|
+
"Routers",
|
|
101
|
+
"RoutingStrategy",
|
|
102
|
+
"Signal",
|
|
103
|
+
"Spawn",
|
|
104
|
+
"SpawnFailed",
|
|
105
|
+
"SpawnFailure",
|
|
106
|
+
"SpawnReply",
|
|
107
|
+
"Spawned",
|
|
108
|
+
"StashBuffer",
|
|
109
|
+
"StashOverflowError",
|
|
110
|
+
"Subscription",
|
|
111
|
+
"SupervisorStrategy",
|
|
112
|
+
"TLSSettings",
|
|
113
|
+
"TapioError",
|
|
114
|
+
"TapioSettings",
|
|
115
|
+
"Terminated",
|
|
116
|
+
"TimerScheduler",
|
|
117
|
+
"WatchError",
|
|
118
|
+
"__version__",
|
|
119
|
+
"register_message",
|
|
120
|
+
"remote_behavior",
|
|
121
|
+
"spawner",
|
|
122
|
+
]
|
tapio/actor/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Actors: refs, paths, behaviors, and the runtime they run in."""
|
|
2
|
+
|
|
3
|
+
from tapio.actor.adapter import AdapterRef
|
|
4
|
+
from tapio.actor.ask import PromiseRef, ask
|
|
5
|
+
from tapio.actor.behavior import (
|
|
6
|
+
AbstractBehavior,
|
|
7
|
+
Behavior,
|
|
8
|
+
Behaviors,
|
|
9
|
+
Directive,
|
|
10
|
+
ReceivingBehavior,
|
|
11
|
+
SetupBehavior,
|
|
12
|
+
SignalHandler,
|
|
13
|
+
Supervise,
|
|
14
|
+
SuperviseBehavior,
|
|
15
|
+
WithStashBehavior,
|
|
16
|
+
WithTimersBehavior,
|
|
17
|
+
)
|
|
18
|
+
from tapio.actor.cell import ActorCell, LocalActorRef
|
|
19
|
+
from tapio.actor.context import ActorContext
|
|
20
|
+
from tapio.actor.dead_letters import (
|
|
21
|
+
DeadLetter,
|
|
22
|
+
DeadLetterOffice,
|
|
23
|
+
DeadLetterReason,
|
|
24
|
+
DeadLetterRef,
|
|
25
|
+
Subscription,
|
|
26
|
+
)
|
|
27
|
+
from tapio.actor.mailbox import Envelope, Mailbox, MailboxConfig, OverflowStrategy
|
|
28
|
+
from tapio.actor.path import ActorPath
|
|
29
|
+
from tapio.actor.ref import ActorRef
|
|
30
|
+
from tapio.actor.router import RoundRobin, Routers, RoutingStrategy
|
|
31
|
+
from tapio.actor.signals import (
|
|
32
|
+
ChildFailed,
|
|
33
|
+
PostStop,
|
|
34
|
+
PreRestart,
|
|
35
|
+
Signal,
|
|
36
|
+
Terminated,
|
|
37
|
+
)
|
|
38
|
+
from tapio.actor.stash import StashBuffer, UnstashBehavior
|
|
39
|
+
from tapio.actor.supervision import Backoff, Decision, SupervisorStrategy
|
|
40
|
+
from tapio.actor.system import ActorSystem, PeerResolver
|
|
41
|
+
from tapio.actor.timers import TimerScheduler
|
|
42
|
+
from tapio.actor.watch import Watcher
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"AbstractBehavior",
|
|
46
|
+
"ActorCell",
|
|
47
|
+
"ActorContext",
|
|
48
|
+
"ActorPath",
|
|
49
|
+
"ActorRef",
|
|
50
|
+
"ActorSystem",
|
|
51
|
+
"AdapterRef",
|
|
52
|
+
"Backoff",
|
|
53
|
+
"Behavior",
|
|
54
|
+
"Behaviors",
|
|
55
|
+
"ChildFailed",
|
|
56
|
+
"DeadLetter",
|
|
57
|
+
"DeadLetterOffice",
|
|
58
|
+
"DeadLetterReason",
|
|
59
|
+
"DeadLetterRef",
|
|
60
|
+
"Decision",
|
|
61
|
+
"Directive",
|
|
62
|
+
"Envelope",
|
|
63
|
+
"LocalActorRef",
|
|
64
|
+
"Mailbox",
|
|
65
|
+
"MailboxConfig",
|
|
66
|
+
"OverflowStrategy",
|
|
67
|
+
"PeerResolver",
|
|
68
|
+
"PostStop",
|
|
69
|
+
"PreRestart",
|
|
70
|
+
"PromiseRef",
|
|
71
|
+
"ReceivingBehavior",
|
|
72
|
+
"RoundRobin",
|
|
73
|
+
"Routers",
|
|
74
|
+
"RoutingStrategy",
|
|
75
|
+
"SetupBehavior",
|
|
76
|
+
"Signal",
|
|
77
|
+
"SignalHandler",
|
|
78
|
+
"StashBuffer",
|
|
79
|
+
"Subscription",
|
|
80
|
+
"Supervise",
|
|
81
|
+
"SuperviseBehavior",
|
|
82
|
+
"SupervisorStrategy",
|
|
83
|
+
"Terminated",
|
|
84
|
+
"TimerScheduler",
|
|
85
|
+
"UnstashBehavior",
|
|
86
|
+
"Watcher",
|
|
87
|
+
"WithStashBehavior",
|
|
88
|
+
"WithTimersBehavior",
|
|
89
|
+
"ask",
|
|
90
|
+
]
|
tapio/actor/adapter.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Message adapters: taking delivery of a protocol you do not own.
|
|
2
|
+
|
|
3
|
+
An actor's declared message type is a contract, and the delivery-time check
|
|
4
|
+
keeps it honest. That creates a problem as soon as two actors written by
|
|
5
|
+
different people talk: the service you called replies with *its* reply type,
|
|
6
|
+
which is not in your protocol. Widening your own type to admit it is wrong
|
|
7
|
+
twice over. It lets anyone send you that message, and it puts a foreign
|
|
8
|
+
vocabulary inside your handlers.
|
|
9
|
+
|
|
10
|
+
An adapter is a ref you hand out instead. It accepts the other protocol's
|
|
11
|
+
message, translates it into one of yours, and delivers the result onto your
|
|
12
|
+
own user lane. A translated message is therefore ordinary traffic: it queues
|
|
13
|
+
where it arrived, it cannot re-enter a running handler, and it is validated
|
|
14
|
+
against your declared type.
|
|
15
|
+
|
|
16
|
+
The translation runs in the owning actor, not in the sender, which is why the
|
|
17
|
+
wrapper below exists instead of applying the function at the send site. The
|
|
18
|
+
function is the owner's code, so a failure in it is the owner's failure and
|
|
19
|
+
becomes a supervision decision, as if the message had been mistranslated
|
|
20
|
+
inside a handler. A sender that knows nothing about the adapter must not have
|
|
21
|
+
the owner's bug raised into it.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from typing import TYPE_CHECKING, Annotated, Any, TypeAlias, TypeVar, cast
|
|
26
|
+
|
|
27
|
+
from pydantic import PlainSerializer, PlainValidator
|
|
28
|
+
|
|
29
|
+
from tapio.actor.dead_letters import Carrier
|
|
30
|
+
from tapio.actor.path import ActorPath
|
|
31
|
+
from tapio.actor.ref import ActorRef
|
|
32
|
+
from tapio.message import Message
|
|
33
|
+
from tapio.remote.address import Address
|
|
34
|
+
from tapio.validation import MessageValidator
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from tapio.actor.cell import ActorCell
|
|
38
|
+
|
|
39
|
+
__all__ = ["AdaptedMessage", "AdapterRef"]
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T", bound=Message)
|
|
42
|
+
U = TypeVar("U", bound=Message)
|
|
43
|
+
|
|
44
|
+
Adapt: TypeAlias = Callable[[Any], Message]
|
|
45
|
+
"""Translates one protocol's message into the owning actor's own."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _carry_adapt(value: object) -> Adapt:
|
|
49
|
+
"""Keep a translation function as itself.
|
|
50
|
+
|
|
51
|
+
Pydantic has no schema for a callable, and this one is not data. It is the
|
|
52
|
+
owner's code travelling with the message so the cell can apply it at the
|
|
53
|
+
right moment. Validation is a callable check, and the object is passed
|
|
54
|
+
through untouched.
|
|
55
|
+
"""
|
|
56
|
+
if callable(value):
|
|
57
|
+
return cast(Adapt, value)
|
|
58
|
+
msg = f"a message adapter carries a callable, not {type(value).__name__}"
|
|
59
|
+
raise ValueError(msg)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _describe_adapt(adapt: Adapt) -> str:
|
|
63
|
+
"""Render a translation function by name, for the rare dump."""
|
|
64
|
+
return getattr(adapt, "__qualname__", None) or repr(adapt)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
AdaptFunction: TypeAlias = Annotated[
|
|
68
|
+
Adapt,
|
|
69
|
+
PlainValidator(_carry_adapt),
|
|
70
|
+
PlainSerializer(_describe_adapt, return_type=str, when_used="always"),
|
|
71
|
+
]
|
|
72
|
+
"""The translation an adapted message carries, kept as the function it is.
|
|
73
|
+
|
|
74
|
+
A function has no data representation, so a dump renders its name instead.
|
|
75
|
+
Nothing in the runtime dumps one. This exists so that a `model_dump` on a
|
|
76
|
+
wrapper caught mid-flight while debugging prints something instead of raising.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AdaptedMessage(Carrier):
|
|
81
|
+
"""One message on its way through an adapter, not yet translated.
|
|
82
|
+
|
|
83
|
+
Internal and short-lived. It exists between the adapter ref that accepted
|
|
84
|
+
a foreign message and the cell that unwraps it on the way into the
|
|
85
|
+
behavior. User code never sees one. A dead letter reports the payload
|
|
86
|
+
rather than this wrapper, because the wrapper is only how the message
|
|
87
|
+
travelled.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
adapt: AdaptFunction
|
|
91
|
+
"""What turns it into one of the owner's messages."""
|
|
92
|
+
|
|
93
|
+
def translate(self) -> Message:
|
|
94
|
+
"""Apply the translation, in the actor that owns it.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The owner's own message.
|
|
98
|
+
"""
|
|
99
|
+
return self.adapt(self.payload)
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
"""Render the payload and the function it is waiting for."""
|
|
103
|
+
return f"AdaptedMessage({self.payload!r}, {_describe_adapt(self.adapt)})"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AdapterRef(ActorRef[U]):
|
|
107
|
+
"""A ref that translates what it is told and delivers it to one actor.
|
|
108
|
+
|
|
109
|
+
Handed out by
|
|
110
|
+
[ActorContext.message_adapter][tapio.actor.context.ActorContext.message_adapter].
|
|
111
|
+
It behaves like any other ref. It never blocks, it is safe to use from any
|
|
112
|
+
thread, and it stays a valid handle after its actor dies. What it cannot
|
|
113
|
+
deliver becomes a dead letter reporting the message the sender sent.
|
|
114
|
+
|
|
115
|
+
It is not an actor. It has no mailbox, no cell and no children, so it
|
|
116
|
+
cannot be watched or asked. Watch the actor that owns it instead.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
__slots__ = ("_adapt", "_cell", "_validate")
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
cell: "ActorCell[Any]",
|
|
125
|
+
path: ActorPath,
|
|
126
|
+
adapt: Adapt,
|
|
127
|
+
validate: MessageValidator,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Bind an adapter to the actor it delivers into.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
cell: The owning actor.
|
|
133
|
+
path: Where this adapter is addressed, under its owner.
|
|
134
|
+
adapt: Translates an accepted message into the owner's own.
|
|
135
|
+
validate: Checks an arriving message against the type this adapter
|
|
136
|
+
declares it accepts.
|
|
137
|
+
"""
|
|
138
|
+
super().__init__(path)
|
|
139
|
+
self._cell = cell
|
|
140
|
+
self._adapt = adapt
|
|
141
|
+
self._validate = validate
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def address(self) -> Address:
|
|
145
|
+
"""The canonical address of the system the owning actor runs in.
|
|
146
|
+
|
|
147
|
+
An adapter is addressable like the actor behind it. Without this it
|
|
148
|
+
would write itself down with no host, and a peer handed one in a
|
|
149
|
+
`reply_to` would read it as a ref with nowhere to dial.
|
|
150
|
+
"""
|
|
151
|
+
return self._cell.runtime.address
|
|
152
|
+
|
|
153
|
+
def tell(self, message: U) -> None:
|
|
154
|
+
"""Accept a message, to be translated and delivered to the owner.
|
|
155
|
+
|
|
156
|
+
The split is the same as any other send. An error about the message
|
|
157
|
+
raises here, on the calling thread, because the sender wrote it.
|
|
158
|
+
Errors about the recipient become dead letters, and so does a
|
|
159
|
+
translation the owner never gets to run.
|
|
160
|
+
|
|
161
|
+
The translation does not happen here. It is the owner's code, so it
|
|
162
|
+
runs in the owner. A failure in it is then the owner's supervision
|
|
163
|
+
decision, not an exception in a caller that has never heard of this
|
|
164
|
+
adapter.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
message: The message to translate and deliver.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
MessageTypeError: If it does not match the type this adapter
|
|
171
|
+
accepts.
|
|
172
|
+
MailboxFullError: If the owner's mailbox is full under
|
|
173
|
+
`OverflowStrategy.FAIL` and the caller is on the system's loop.
|
|
174
|
+
pydantic.ValidationError: If content validation is on and the
|
|
175
|
+
message does not satisfy its own model.
|
|
176
|
+
"""
|
|
177
|
+
self._validate(message)
|
|
178
|
+
envelope = AdaptedMessage(payload=message, adapt=self._adapt)
|
|
179
|
+
cell = self._cell
|
|
180
|
+
dispatcher = cell.runtime.dispatcher
|
|
181
|
+
if dispatcher.is_current():
|
|
182
|
+
cell.deliver(envelope)
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
dispatcher.call_soon_threadsafe(cell.deliver_offloop, envelope)
|
|
186
|
+
except RuntimeError:
|
|
187
|
+
cell.log.warning(
|
|
188
|
+
"dead letter: %s sent to an adapter after the loop closed",
|
|
189
|
+
type(message).__name__,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def offer(self, message: U) -> None:
|
|
193
|
+
"""Accept a message, waiting for the owner's mailbox to have room.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
message: The message to translate and deliver.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
MessageTypeError: If it does not match the type this adapter
|
|
200
|
+
accepts.
|
|
201
|
+
RuntimeError: If called off the system's loop, as for any `offer`.
|
|
202
|
+
pydantic.ValidationError: If content validation is on and the
|
|
203
|
+
message does not satisfy its own model.
|
|
204
|
+
"""
|
|
205
|
+
self._validate(message)
|
|
206
|
+
cell = self._cell
|
|
207
|
+
if not cell.runtime.dispatcher.is_current():
|
|
208
|
+
msg = (
|
|
209
|
+
f"offer to the adapter at {self.path} must run on the system's "
|
|
210
|
+
"loop; `tell` is the thread-safe send"
|
|
211
|
+
)
|
|
212
|
+
raise RuntimeError(msg)
|
|
213
|
+
await cell.offer(AdaptedMessage(payload=message, adapt=self._adapt))
|
|
214
|
+
|
|
215
|
+
def __repr__(self) -> str:
|
|
216
|
+
"""Render the adapter, its owner, and what it translates with."""
|
|
217
|
+
return f"AdapterRef({str(self.path)!r}, adapt={_describe_adapt(self._adapt)})"
|