durable-actors 0.5.5__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.
- durable_actors-0.5.5/.gitignore +22 -0
- durable_actors-0.5.5/LICENSE.md +9 -0
- durable_actors-0.5.5/PKG-INFO +111 -0
- durable_actors-0.5.5/README.md +90 -0
- durable_actors-0.5.5/pyproject.toml +44 -0
- durable_actors-0.5.5/src/durable_actors/__init__.py +25 -0
- durable_actors-0.5.5/src/durable_actors/actor.py +145 -0
- durable_actors-0.5.5/src/durable_actors/build.py +237 -0
- durable_actors-0.5.5/src/durable_actors/client.py +512 -0
- durable_actors-0.5.5/src/durable_actors/codegen.py +781 -0
- durable_actors-0.5.5/src/durable_actors/connection.py +158 -0
- durable_actors-0.5.5/src/durable_actors/contract.py +390 -0
- durable_actors-0.5.5/src/durable_actors/executor_wire.py +41 -0
- durable_actors-0.5.5/src/durable_actors/fields.py +61 -0
- durable_actors-0.5.5/src/durable_actors/generated.py +44 -0
- durable_actors-0.5.5/src/durable_actors/guards.py +20 -0
- durable_actors-0.5.5/src/durable_actors/host.py +202 -0
- durable_actors-0.5.5/src/durable_actors/json.py +14 -0
- durable_actors-0.5.5/src/durable_actors/proxy.py +45 -0
- durable_actors-0.5.5/src/durable_actors/py.typed +0 -0
- durable_actors-0.5.5/src/durable_actors/reference.py +89 -0
- durable_actors-0.5.5/src/durable_actors/runtime.py +282 -0
- durable_actors-0.5.5/src/durable_actors/sandbox.py +87 -0
- durable_actors-0.5.5/src/durable_actors/session.py +203 -0
- durable_actors-0.5.5/src/durable_actors/socket.py +287 -0
- durable_actors-0.5.5/src/durable_actors/subscription.py +142 -0
- durable_actors-0.5.5/src/durable_actors/supervisor.py +233 -0
- durable_actors-0.5.5/tests/fixtures/documented.py +33 -0
- durable_actors-0.5.5/tests/fixtures/effects.py +14 -0
- durable_actors-0.5.5/tests/fixtures/field_types.py +37 -0
- durable_actors-0.5.5/tests/fixtures/image_smoke.py +71 -0
- durable_actors-0.5.5/tests/fixtures/types.py +46 -0
- durable_actors-0.5.5/tests/test_authoring.py +261 -0
- durable_actors-0.5.5/tests/test_build.py +189 -0
- durable_actors-0.5.5/tests/test_client.py +81 -0
- durable_actors-0.5.5/tests/test_codegen.py +559 -0
- durable_actors-0.5.5/tests/test_connection.py +50 -0
- durable_actors-0.5.5/tests/test_host.py +239 -0
- durable_actors-0.5.5/tests/test_integration.py +268 -0
- durable_actors-0.5.5/tests/test_parity.py +280 -0
- durable_actors-0.5.5/tests/test_runtime.py +314 -0
- durable_actors-0.5.5/tests/test_sandbox.py +98 -0
- durable_actors-0.5.5/tests/test_session.py +161 -0
- durable_actors-0.5.5/tests/test_socket.py +32 -0
- durable_actors-0.5.5/tests/test_subscription.py +161 -0
- durable_actors-0.5.5/uv.lock +1215 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
sdk/node_modules/
|
|
3
|
+
sdk/dist/
|
|
4
|
+
sdk/.test-dist/
|
|
5
|
+
packages/*/dist/
|
|
6
|
+
sdk/src/generated/
|
|
7
|
+
target/
|
|
8
|
+
dist-runtime/
|
|
9
|
+
.durable-actors/
|
|
10
|
+
.artifacts/
|
|
11
|
+
.env
|
|
12
|
+
.env.*
|
|
13
|
+
!.env.example
|
|
14
|
+
.DS_Store
|
|
15
|
+
Thumbs.db
|
|
16
|
+
|
|
17
|
+
.venv/
|
|
18
|
+
__pycache__/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
sdk-python/dist/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Terse
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: durable-actors
|
|
3
|
+
Version: 0.5.5
|
|
4
|
+
Summary: Typed Python durable actors and generated clients
|
|
5
|
+
Project-URL: Repository, https://github.com/TerseAI/durable-actors
|
|
6
|
+
Project-URL: Documentation, https://github.com/TerseAI/durable-actors/blob/main/docs/reference/python.md
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE.md
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: httpx<1,>=0.28
|
|
11
|
+
Requires-Dist: jsonschema<5,>=4.23
|
|
12
|
+
Requires-Dist: packaging<27,>=24
|
|
13
|
+
Requires-Dist: pydantic<3,>=2.12
|
|
14
|
+
Requires-Dist: typing-extensions<5,>=4.12
|
|
15
|
+
Requires-Dist: websockets<17,>=15
|
|
16
|
+
Provides-Extra: codegen
|
|
17
|
+
Requires-Dist: datamodel-code-generator<0.84,>=0.83; extra == 'codegen'
|
|
18
|
+
Requires-Dist: mypy<2,>=1.15; extra == 'codegen'
|
|
19
|
+
Requires-Dist: pip<27,>=25; extra == 'codegen'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Little Actors for Python
|
|
23
|
+
|
|
24
|
+
Define durable actors in Python and generate typed Python clients. Python 3.11+ is supported. Actor state, placement, routing, and WebSocket delivery use the same Rust runtime as the TypeScript SDK.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pnpm dlx durable-actors init my-actors --template python
|
|
28
|
+
cd my-actors
|
|
29
|
+
pnpm install
|
|
30
|
+
uv sync
|
|
31
|
+
pnpm exec durable-actors dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The shared TypeScript CLI manages development, generation, and Python type checking. It selects the project's `.venv` (or the active environment); `DURABLE_ACTORS_PYTHON` overrides the interpreter. Install Node.js 22+, Python 3.11+, and uv. The CLI downloads the matching native runtime on macOS and Linux. When developing this repository, set `DURABLE_ACTORS_BINARY` to an absolute path to your `cargo build --locked` executable.
|
|
35
|
+
|
|
36
|
+
## Define actors
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from pydantic import BaseModel
|
|
40
|
+
from durable_actors import Actor, emitted, ephemeral
|
|
41
|
+
|
|
42
|
+
class Message(BaseModel):
|
|
43
|
+
text: str
|
|
44
|
+
|
|
45
|
+
class Chat(Actor):
|
|
46
|
+
count: int = 0
|
|
47
|
+
messages: list[Message] = emitted(default_factory=list)
|
|
48
|
+
busy: bool = ephemeral(False)
|
|
49
|
+
|
|
50
|
+
def append(self, message: Message) -> list[Message]:
|
|
51
|
+
self.messages.append(message)
|
|
52
|
+
return self.messages
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Public `def` methods become synchronous RPCs. Annotated fields persist by default. `emitted()` persists a field and broadcasts its saved changes; `ephemeral()` keeps a field temporary. Mutable defaults are copied for each actor, and both helpers accept `default_factory` for values constructed on activation. Use `ephemeral(default_factory=...)` for locks, caches, and service clients, and `ClassVar` for class constants. Classes extend `Actor` directly and use field defaults instead of constructors. Prefix helper methods with `_`. Synchronous handlers run on a worker thread, with calls serialized per actor. Socket hooks can also use ordinary `def`; `self.get_connections()` returns typed sockets.
|
|
56
|
+
|
|
57
|
+
Add `@reentrant` (imported from `durable_actors`) to a `def` method to let other invocations enter before it finishes. Synchronous reentrant handlers overlap on worker threads; coordinate shared mutations and keep blocking I/O outside shared locks. Ordinary calls still serialize with each other. As in TypeScript, enabling reentrancy disables error rollback for the entire actor class. See the [execution semantics](../docs/reference/python.md#execution-and-failures) for details.
|
|
58
|
+
|
|
59
|
+
## Generate and use a client
|
|
60
|
+
|
|
61
|
+
With the actor server running, generate clients in your application:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
pnpm add -D durable-actors
|
|
65
|
+
uv add 'durable-actors[codegen]'
|
|
66
|
+
pnpm exec durable-actors generate --out-dir generated
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can also generate directly from a trusted source entrypoint:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pnpm exec durable-actors generate src/actors.py --out-dir generated
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from generated import actors
|
|
77
|
+
|
|
78
|
+
chat = actors.Chat.get("lobby")
|
|
79
|
+
messages: actors.Chat.Methods.append.Result = chat.append(actors.Chat.Message(text="hello"))
|
|
80
|
+
print(messages[0].text)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The CLI runs strict mypy on local actor definitions before generation and on the generated package afterward. `dev` checks definitions before startup and every reload; an invalid edit leaves the previous code running.
|
|
84
|
+
|
|
85
|
+
Generated RPC methods return typed values directly. The SDK creates a shared HTTP client when needed, reads configuration from the environment, and closes its connection pool at process exit. You only need the actor ID.
|
|
86
|
+
|
|
87
|
+
The generated package exposes `actors`, matching the TypeScript client namespace. Use `actors.Chat.get(id)` for a handle, `actors.Chat.Stub` for its type, and `actors.Chat.Methods.append.Args` / `.Result` for method types. Socket types live at `actors.Chat.Metadata`, `.Incoming`, `.Outgoing`, and `.State`; concrete models such as `actors.Chat.Message` are also available there. The package includes docstrings, independent Pydantic models, and `py.typed`. Consumers need only `durable-actors`, not the actor project or the code generator. Regenerate after changing the actor contract, and include the generated package in your application's type checks.
|
|
88
|
+
|
|
89
|
+
Typing uses inline annotations and the [PEP 561](https://peps.python.org/pep-0561/) package marker. Both mypy and Pyright check the SDK and generated clients. Pydantic validates inputs, outputs, and persisted state at runtime. Python annotations remain ordinary annotations: `chat.append(42)` is rejected by a type checker and by runtime validation.
|
|
90
|
+
|
|
91
|
+
## Subscribe to state
|
|
92
|
+
|
|
93
|
+
Actors with emitted fields have a typed `subscribe` method:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
chat = actors.Chat.get("lobby")
|
|
97
|
+
subscription = chat.subscribe(lambda state: print(state.messages))
|
|
98
|
+
chat.append(actors.Chat.Message(text="hello"))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The SDK receives the initial state and applies later patches in the background. Each callback gets a complete typed snapshot of the emitted fields, and RPC calls continue normally. Call `subscription.close()` when finished. Callbacks run serially on a background thread; the subscription does not keep an otherwise finished process alive.
|
|
102
|
+
|
|
103
|
+
Pass `on_error=handler` to handle connection, validation, or callback failures. A failure stops the subscription; its exception is available as `subscription.error` and is logged if no handler is supplied. Actors with required connection metadata also require `metadata=...` when subscribing.
|
|
104
|
+
|
|
105
|
+
See the [Python reference](https://github.com/TerseAI/durable-actors/blob/main/docs/reference/python.md) for supported types, sockets, reentrancy, deployment, and CLI options.
|
|
106
|
+
|
|
107
|
+
## Resource settings and backend helpers
|
|
108
|
+
|
|
109
|
+
Use `@sandbox(cpu=2, memory_mib=2048, idle_timeout_ms=60_000, regions=["canada"])` above an actor class to override deployment defaults. Import `sandbox` from `durable_actors`.
|
|
110
|
+
|
|
111
|
+
Source classes also support `Chat.get("lobby")` with typed synchronous methods, including calls from other actors. Generated handles expose `broadcast(message)`; `actors.Chat.Authorization`, `actors.Chat.prepare_websocket(...)`, and `ActorProxy.handle(...)` issue typed browser access grants. `ActorSessionTransport` renews short-lived application credentials. The [Python reference](../docs/reference/python.md) covers these APIs and their docstrings.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Little Actors for Python
|
|
2
|
+
|
|
3
|
+
Define durable actors in Python and generate typed Python clients. Python 3.11+ is supported. Actor state, placement, routing, and WebSocket delivery use the same Rust runtime as the TypeScript SDK.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm dlx durable-actors init my-actors --template python
|
|
7
|
+
cd my-actors
|
|
8
|
+
pnpm install
|
|
9
|
+
uv sync
|
|
10
|
+
pnpm exec durable-actors dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The shared TypeScript CLI manages development, generation, and Python type checking. It selects the project's `.venv` (or the active environment); `DURABLE_ACTORS_PYTHON` overrides the interpreter. Install Node.js 22+, Python 3.11+, and uv. The CLI downloads the matching native runtime on macOS and Linux. When developing this repository, set `DURABLE_ACTORS_BINARY` to an absolute path to your `cargo build --locked` executable.
|
|
14
|
+
|
|
15
|
+
## Define actors
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
from durable_actors import Actor, emitted, ephemeral
|
|
20
|
+
|
|
21
|
+
class Message(BaseModel):
|
|
22
|
+
text: str
|
|
23
|
+
|
|
24
|
+
class Chat(Actor):
|
|
25
|
+
count: int = 0
|
|
26
|
+
messages: list[Message] = emitted(default_factory=list)
|
|
27
|
+
busy: bool = ephemeral(False)
|
|
28
|
+
|
|
29
|
+
def append(self, message: Message) -> list[Message]:
|
|
30
|
+
self.messages.append(message)
|
|
31
|
+
return self.messages
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Public `def` methods become synchronous RPCs. Annotated fields persist by default. `emitted()` persists a field and broadcasts its saved changes; `ephemeral()` keeps a field temporary. Mutable defaults are copied for each actor, and both helpers accept `default_factory` for values constructed on activation. Use `ephemeral(default_factory=...)` for locks, caches, and service clients, and `ClassVar` for class constants. Classes extend `Actor` directly and use field defaults instead of constructors. Prefix helper methods with `_`. Synchronous handlers run on a worker thread, with calls serialized per actor. Socket hooks can also use ordinary `def`; `self.get_connections()` returns typed sockets.
|
|
35
|
+
|
|
36
|
+
Add `@reentrant` (imported from `durable_actors`) to a `def` method to let other invocations enter before it finishes. Synchronous reentrant handlers overlap on worker threads; coordinate shared mutations and keep blocking I/O outside shared locks. Ordinary calls still serialize with each other. As in TypeScript, enabling reentrancy disables error rollback for the entire actor class. See the [execution semantics](../docs/reference/python.md#execution-and-failures) for details.
|
|
37
|
+
|
|
38
|
+
## Generate and use a client
|
|
39
|
+
|
|
40
|
+
With the actor server running, generate clients in your application:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pnpm add -D durable-actors
|
|
44
|
+
uv add 'durable-actors[codegen]'
|
|
45
|
+
pnpm exec durable-actors generate --out-dir generated
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You can also generate directly from a trusted source entrypoint:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pnpm exec durable-actors generate src/actors.py --out-dir generated
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from generated import actors
|
|
56
|
+
|
|
57
|
+
chat = actors.Chat.get("lobby")
|
|
58
|
+
messages: actors.Chat.Methods.append.Result = chat.append(actors.Chat.Message(text="hello"))
|
|
59
|
+
print(messages[0].text)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The CLI runs strict mypy on local actor definitions before generation and on the generated package afterward. `dev` checks definitions before startup and every reload; an invalid edit leaves the previous code running.
|
|
63
|
+
|
|
64
|
+
Generated RPC methods return typed values directly. The SDK creates a shared HTTP client when needed, reads configuration from the environment, and closes its connection pool at process exit. You only need the actor ID.
|
|
65
|
+
|
|
66
|
+
The generated package exposes `actors`, matching the TypeScript client namespace. Use `actors.Chat.get(id)` for a handle, `actors.Chat.Stub` for its type, and `actors.Chat.Methods.append.Args` / `.Result` for method types. Socket types live at `actors.Chat.Metadata`, `.Incoming`, `.Outgoing`, and `.State`; concrete models such as `actors.Chat.Message` are also available there. The package includes docstrings, independent Pydantic models, and `py.typed`. Consumers need only `durable-actors`, not the actor project or the code generator. Regenerate after changing the actor contract, and include the generated package in your application's type checks.
|
|
67
|
+
|
|
68
|
+
Typing uses inline annotations and the [PEP 561](https://peps.python.org/pep-0561/) package marker. Both mypy and Pyright check the SDK and generated clients. Pydantic validates inputs, outputs, and persisted state at runtime. Python annotations remain ordinary annotations: `chat.append(42)` is rejected by a type checker and by runtime validation.
|
|
69
|
+
|
|
70
|
+
## Subscribe to state
|
|
71
|
+
|
|
72
|
+
Actors with emitted fields have a typed `subscribe` method:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
chat = actors.Chat.get("lobby")
|
|
76
|
+
subscription = chat.subscribe(lambda state: print(state.messages))
|
|
77
|
+
chat.append(actors.Chat.Message(text="hello"))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The SDK receives the initial state and applies later patches in the background. Each callback gets a complete typed snapshot of the emitted fields, and RPC calls continue normally. Call `subscription.close()` when finished. Callbacks run serially on a background thread; the subscription does not keep an otherwise finished process alive.
|
|
81
|
+
|
|
82
|
+
Pass `on_error=handler` to handle connection, validation, or callback failures. A failure stops the subscription; its exception is available as `subscription.error` and is logged if no handler is supplied. Actors with required connection metadata also require `metadata=...` when subscribing.
|
|
83
|
+
|
|
84
|
+
See the [Python reference](https://github.com/TerseAI/durable-actors/blob/main/docs/reference/python.md) for supported types, sockets, reentrancy, deployment, and CLI options.
|
|
85
|
+
|
|
86
|
+
## Resource settings and backend helpers
|
|
87
|
+
|
|
88
|
+
Use `@sandbox(cpu=2, memory_mib=2048, idle_timeout_ms=60_000, regions=["canada"])` above an actor class to override deployment defaults. Import `sandbox` from `durable_actors`.
|
|
89
|
+
|
|
90
|
+
Source classes also support `Chat.get("lobby")` with typed synchronous methods, including calls from other actors. Generated handles expose `broadcast(message)`; `actors.Chat.Authorization`, `actors.Chat.prepare_websocket(...)`, and `ActorProxy.handle(...)` issue typed browser access grants. `ActorSessionTransport` renews short-lived application credentials. The [Python reference](../docs/reference/python.md) covers these APIs and their docstrings.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "durable-actors"
|
|
7
|
+
version = "0.5.5"
|
|
8
|
+
description = "Typed Python durable actors and generated clients"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["pydantic>=2.12,<3", "typing-extensions>=4.12,<5", "httpx>=0.28,<1", "websockets>=15,<17", "jsonschema>=4.23,<5", "packaging>=24,<27"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Repository = "https://github.com/TerseAI/durable-actors"
|
|
16
|
+
Documentation = "https://github.com/TerseAI/durable-actors/blob/main/docs/reference/python.md"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
codegen = ["datamodel-code-generator>=0.83,<0.84", "mypy>=1.15,<2", "pip>=25,<27"]
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = ["pytest>=8,<10", "pytest-asyncio>=1,<2", "mypy>=1.15,<2", "pyright>=1.1.400,<2", "ruff>=0.11,<1", "types-jsonschema", "datamodel-code-generator>=0.83,<0.84", "build>=1,<2", "pip>=25,<27"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
asyncio_mode = "auto"
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
|
|
28
|
+
[tool.mypy]
|
|
29
|
+
strict = true
|
|
30
|
+
files = ["src/durable_actors"]
|
|
31
|
+
|
|
32
|
+
[tool.pyright]
|
|
33
|
+
include = ["src/durable_actors"]
|
|
34
|
+
typeCheckingMode = "strict"
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py311"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/durable_actors"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Define durable Python actors and use synchronous typed clients and subscriptions."""
|
|
2
|
+
|
|
3
|
+
from .actor import Actor as Actor
|
|
4
|
+
from .actor import reentrant as reentrant
|
|
5
|
+
from .client import ActorInvocationError as ActorInvocationError
|
|
6
|
+
from .client import ActorProtocolError as ActorProtocolError
|
|
7
|
+
from .client import ActorTransport as ActorTransport
|
|
8
|
+
from .client import Client as Client
|
|
9
|
+
from .client import SocketGrant as SocketGrant
|
|
10
|
+
from .connection import StateSnapshot as StateSnapshot
|
|
11
|
+
from .connection import StateUpdate as StateUpdate
|
|
12
|
+
from .fields import emitted as emitted
|
|
13
|
+
from .fields import ephemeral as ephemeral
|
|
14
|
+
from .generated import UNSET as UNSET
|
|
15
|
+
from .generated import Unset as Unset
|
|
16
|
+
from .json import JsonValue as JsonValue
|
|
17
|
+
from .proxy import SocketAuthorization as SocketAuthorization
|
|
18
|
+
from .sandbox import SandboxOptions as SandboxOptions
|
|
19
|
+
from .sandbox import SandboxRegion as SandboxRegion
|
|
20
|
+
from .sandbox import sandbox as sandbox
|
|
21
|
+
from .session import ActorSession as ActorSession
|
|
22
|
+
from .session import ActorSessionRejectedError as ActorSessionRejectedError
|
|
23
|
+
from .session import ActorSessionTransport as ActorSessionTransport
|
|
24
|
+
from .socket import ActorSocket as ActorSocket
|
|
25
|
+
from .subscription import Subscription as Subscription
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Define durable actors and control invocation concurrency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, Self, TypeVar
|
|
8
|
+
|
|
9
|
+
from typing_extensions import TypeVar as DefaultTypeVar
|
|
10
|
+
|
|
11
|
+
from .json import JsonValue
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .client import ActorTransport
|
|
15
|
+
from .connection import Connection
|
|
16
|
+
from .socket import ActorSocket
|
|
17
|
+
|
|
18
|
+
Metadata = DefaultTypeVar("Metadata", default=JsonValue)
|
|
19
|
+
Incoming = DefaultTypeVar("Incoming", default=JsonValue)
|
|
20
|
+
Outgoing = DefaultTypeVar("Outgoing", default=Incoming)
|
|
21
|
+
Tag = DefaultTypeVar("Tag", bound=str, default=str)
|
|
22
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Actor(Generic[Metadata, Incoming, Outgoing, Tag]):
|
|
26
|
+
"""Base class for durable actors with typed RPCs and WebSocket hooks.
|
|
27
|
+
|
|
28
|
+
Public def methods become synchronous RPCs. Annotated fields persist by
|
|
29
|
+
default; emitted() also broadcasts their saved changes, and ephemeral() excludes
|
|
30
|
+
temporary values. Use field defaults or factories instead of a constructor.
|
|
31
|
+
|
|
32
|
+
Generic parameters describe connection metadata, incoming application
|
|
33
|
+
messages, outgoing application messages, and allowed connection tags. Outgoing
|
|
34
|
+
defaults to Incoming, tags to str, and other parameters to JsonValue.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
"""Initialize independent field defaults; the runtime restores persisted values afterward."""
|
|
39
|
+
from .contract import describe_actor
|
|
40
|
+
|
|
41
|
+
for field in describe_actor(type(self)).fields.values():
|
|
42
|
+
value = (
|
|
43
|
+
field.default_factory()
|
|
44
|
+
if field.default_factory is not None
|
|
45
|
+
else deepcopy(field.default)
|
|
46
|
+
)
|
|
47
|
+
setattr(self, field.name, value)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get(cls, actor_id: str, transport: ActorTransport | None = None) -> Self:
|
|
51
|
+
"""Return a typed synchronous reference without activating the actor locally.
|
|
52
|
+
|
|
53
|
+
RPC signatures match the source class. Fields and lifecycle hooks belong
|
|
54
|
+
to the running actor; read state through RPCs or generated subscriptions.
|
|
55
|
+
"""
|
|
56
|
+
from .reference import actor_reference
|
|
57
|
+
|
|
58
|
+
return actor_reference(cls, actor_id, transport)
|
|
59
|
+
|
|
60
|
+
def connect(self, metadata: Metadata) -> Connection[Incoming, Outgoing, JsonValue, JsonValue]:
|
|
61
|
+
"""Open a typed WebSocket through a source-class reference from get()."""
|
|
62
|
+
raise RuntimeError("connect() requires an actor reference from get()")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def id(self) -> str:
|
|
66
|
+
"""Identity of this actor, available during an active invocation or socket hook."""
|
|
67
|
+
from .socket import current_scope
|
|
68
|
+
|
|
69
|
+
return current_scope(self).actor_id
|
|
70
|
+
|
|
71
|
+
def on_connect(self, socket: ActorSocket[Metadata, Outgoing, Tag]) -> None:
|
|
72
|
+
"""Handle a new WebSocket connection before it is accepted.
|
|
73
|
+
|
|
74
|
+
Override with def to inspect metadata, set tags, send a welcome
|
|
75
|
+
message, or reject with socket.reject(). The default accepts the connection.
|
|
76
|
+
"""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def on_message(self, socket: ActorSocket[Metadata, Outgoing, Tag], message: Incoming) -> None:
|
|
80
|
+
"""Handle a validated incoming application message from a connected client.
|
|
81
|
+
|
|
82
|
+
Override with def to update actor state or send typed replies
|
|
83
|
+
through socket. The default ignores application messages.
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def on_disconnect(
|
|
88
|
+
self, socket: ActorSocket[Metadata, Outgoing, Tag], code: int, reason: str, was_clean: bool
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Handle a closed connection; override with def for cleanup.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
socket: Connection with its last known metadata and tags.
|
|
94
|
+
code: WebSocket close status code.
|
|
95
|
+
reason: WebSocket close reason.
|
|
96
|
+
was_clean: Whether the connection completed a clean closing handshake.
|
|
97
|
+
"""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def get_connections(self) -> list[ActorSocket[Metadata, Outgoing, Tag]]:
|
|
101
|
+
"""Return connected sockets from a synchronous actor method or hook.
|
|
102
|
+
|
|
103
|
+
Use the returned handles only during the current invocation.
|
|
104
|
+
"""
|
|
105
|
+
from .socket import current_scope
|
|
106
|
+
|
|
107
|
+
scope = current_scope(self)
|
|
108
|
+
return scope.blocking(scope.get_connections)
|
|
109
|
+
|
|
110
|
+
def broadcast(
|
|
111
|
+
self,
|
|
112
|
+
message: Outgoing,
|
|
113
|
+
*,
|
|
114
|
+
except_ids: tuple[str, ...] = (),
|
|
115
|
+
tags: tuple[Tag, ...] = (),
|
|
116
|
+
tag_match: Literal["all", "any"] = "all",
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Queue a typed application message for selected connections.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
message: Value matching the actor's outgoing message type.
|
|
122
|
+
except_ids: Connection IDs to exclude.
|
|
123
|
+
tags: Restrict delivery to matching tags; empty selects all connections.
|
|
124
|
+
tag_match: Use "all" to require every tag or "any" to require one.
|
|
125
|
+
|
|
126
|
+
Must be called during an active actor invocation or socket hook.
|
|
127
|
+
"""
|
|
128
|
+
from .socket import current_scope
|
|
129
|
+
|
|
130
|
+
current_scope(self).broadcast(message, except_ids, tags, tag_match)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def reentrant(method: F) -> F:
|
|
134
|
+
"""Allow other invocations to enter before this method finishes.
|
|
135
|
+
|
|
136
|
+
Works with def RPCs and socket hooks. Handlers overlap on worker threads.
|
|
137
|
+
Ordinary invocations still serialize with each other and block new entries.
|
|
138
|
+
Nested method calls inherit the outer invocation's admission policy.
|
|
139
|
+
|
|
140
|
+
Reentrant actors share a live instance. Coordinate shared mutable state
|
|
141
|
+
across overlapping handlers. Failed invocations do not roll back state
|
|
142
|
+
anywhere in the actor class, because that could erase another call's work.
|
|
143
|
+
"""
|
|
144
|
+
setattr(method, "__actor_reentrant__", True)
|
|
145
|
+
return method
|