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.
Files changed (53) hide show
  1. tapio/__init__.py +122 -0
  2. tapio/actor/__init__.py +90 -0
  3. tapio/actor/adapter.py +217 -0
  4. tapio/actor/ask.py +375 -0
  5. tapio/actor/behavior.py +719 -0
  6. tapio/actor/cell.py +1403 -0
  7. tapio/actor/context.py +264 -0
  8. tapio/actor/dead_letters.py +370 -0
  9. tapio/actor/events.py +112 -0
  10. tapio/actor/mailbox.py +350 -0
  11. tapio/actor/path.py +92 -0
  12. tapio/actor/ref.py +193 -0
  13. tapio/actor/router.py +239 -0
  14. tapio/actor/signals.py +82 -0
  15. tapio/actor/stash.py +148 -0
  16. tapio/actor/supervision.py +231 -0
  17. tapio/actor/system.py +610 -0
  18. tapio/actor/timers.py +260 -0
  19. tapio/actor/watch.py +96 -0
  20. tapio/dispatch/__init__.py +10 -0
  21. tapio/dispatch/blocking.py +174 -0
  22. tapio/dispatch/dispatcher.py +76 -0
  23. tapio/errors.py +197 -0
  24. tapio/logging.py +57 -0
  25. tapio/message.py +30 -0
  26. tapio/py.typed +0 -0
  27. tapio/remote/__init__.py +10 -0
  28. tapio/remote/address.py +179 -0
  29. tapio/remote/association.py +1008 -0
  30. tapio/remote/codec.py +418 -0
  31. tapio/remote/context.py +108 -0
  32. tapio/remote/endpoint.py +729 -0
  33. tapio/remote/failure.py +195 -0
  34. tapio/remote/handshake.py +310 -0
  35. tapio/remote/protocol.py +37 -0
  36. tapio/remote/ref.py +289 -0
  37. tapio/remote/registry.py +165 -0
  38. tapio/remote/spawner.py +555 -0
  39. tapio/remote/transport.py +489 -0
  40. tapio/settings.py +199 -0
  41. tapio/testkit/__init__.py +47 -0
  42. tapio/testkit/behavior.py +499 -0
  43. tapio/testkit/leaks.py +69 -0
  44. tapio/testkit/plugin.py +104 -0
  45. tapio/testkit/probe.py +332 -0
  46. tapio/testkit/remote.py +329 -0
  47. tapio/validation.py +155 -0
  48. tapio/version.py +34 -0
  49. tapio_py-0.1.0.dist-info/METADATA +273 -0
  50. tapio_py-0.1.0.dist-info/RECORD +53 -0
  51. tapio_py-0.1.0.dist-info/WHEEL +4 -0
  52. tapio_py-0.1.0.dist-info/entry_points.txt +2 -0
  53. 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
+ ]
@@ -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)})"