plexspaces 0.1.1__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.
- plexspaces-0.1.1/PKG-INFO +505 -0
- plexspaces-0.1.1/README.md +478 -0
- plexspaces-0.1.1/plexspaces/__init__.py +80 -0
- plexspaces-0.1.1/plexspaces/actor_id.py +114 -0
- plexspaces-0.1.1/plexspaces/decorators.py +691 -0
- plexspaces-0.1.1/plexspaces/generated/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/buf/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/buf/validate/__init__.py +3799 -0
- plexspaces-0.1.1/plexspaces/generated/google/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/google/api/__init__.py +379 -0
- plexspaces-0.1.1/plexspaces/generated/grpc/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/grpc/gateway/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/grpc/gateway/protoc_gen_openapiv2/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/grpc/gateway/protoc_gen_openapiv2/options/__init__.py +1022 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/actor/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/actor/v1/__init__.py +3538 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/application/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/application/v1/__init__.py +827 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/behaviors/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/behaviors/v1/__init__.py +63 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/channel/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/channel/v1/__init__.py +915 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/circuitbreaker/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/circuitbreaker/prv/__init__.py +398 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/common/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/common/v1/__init__.py +691 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/config/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/config/v1/__init__.py +109 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/dashboard/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/dashboard/v1/__init__.py +647 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/facets/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/facets/v1/__init__.py +333 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/firecracker/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/firecracker/v1/__init__.py +890 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/grpc/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/grpc/v1/__init__.py +679 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/journaling/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/journaling/v1/__init__.py +1006 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/keyvalue/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/keyvalue/v1/__init__.py +1007 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/locks/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/locks/prv/__init__.py +122 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/mailbox/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/mailbox/v1/__init__.py +246 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/metrics/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/metrics/v1/__init__.py +878 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/node/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/node/v1/__init__.py +2564 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/object_registry/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/object_registry/v1/__init__.py +759 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/persistence/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/persistence/prv/__init__.py +539 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/pool/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/pool/v1/__init__.py +791 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/processgroups/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/processgroups/v1/__init__.py +622 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/scheduling/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/scheduling/v1/__init__.py +364 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/security/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/security/v1/__init__.py +254 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/services/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/services/prv/__init__.py +112 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/storage/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/storage/v1/__init__.py +747 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/supervision/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/supervision/v1/__init__.py +536 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/system/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/system/v1/__init__.py +1602 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/timer/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/timer/v1/__init__.py +402 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/tuplespace/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/tuplespace/v1/__init__.py +869 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/wasm/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/wasm/v1/__init__.py +920 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/workflow/__init__.py +0 -0
- plexspaces-0.1.1/plexspaces/generated/plexspaces/workflow/v1/__init__.py +870 -0
- plexspaces-0.1.1/plexspaces/host.py +2093 -0
- plexspaces-0.1.1/plexspaces/leader_worker.py +148 -0
- plexspaces-0.1.1/plexspaces/proto_wire.py +1357 -0
- plexspaces-0.1.1/plexspaces/runtime.py +284 -0
- plexspaces-0.1.1/plexspaces/workflow.py +105 -0
- plexspaces-0.1.1/plexspaces.egg-info/PKG-INFO +505 -0
- plexspaces-0.1.1/plexspaces.egg-info/SOURCES.txt +101 -0
- plexspaces-0.1.1/plexspaces.egg-info/dependency_links.txt +1 -0
- plexspaces-0.1.1/plexspaces.egg-info/entry_points.txt +2 -0
- plexspaces-0.1.1/plexspaces.egg-info/requires.txt +10 -0
- plexspaces-0.1.1/plexspaces.egg-info/top_level.txt +2 -0
- plexspaces-0.1.1/plexspaces_cli/__init__.py +8 -0
- plexspaces-0.1.1/plexspaces_cli/build.py +374 -0
- plexspaces-0.1.1/pyproject.toml +56 -0
- plexspaces-0.1.1/setup.cfg +4 -0
- plexspaces-0.1.1/tests/test_abstractions_contract.py +166 -0
- plexspaces-0.1.1/tests/test_actor_id.py +78 -0
- plexspaces-0.1.1/tests/test_application_metrics_host.py +37 -0
- plexspaces-0.1.1/tests/test_decorators.py +680 -0
- plexspaces-0.1.1/tests/test_host.py +498 -0
- plexspaces-0.1.1/tests/test_host_collective.py +180 -0
- plexspaces-0.1.1/tests/test_host_tuple_space.py +106 -0
- plexspaces-0.1.1/tests/test_http_fetch_wire.py +31 -0
- plexspaces-0.1.1/tests/test_lock_wire.py +75 -0
- plexspaces-0.1.1/tests/test_runtime.py +154 -0
- plexspaces-0.1.1/tests/test_workflow_retry.py +115 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plexspaces
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: PlexSpaces Python SDK - Build actors with minimal boilerplate
|
|
5
|
+
Author: PlexSpaces Contributors
|
|
6
|
+
License: AGPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/plexspaces/plexspaces
|
|
8
|
+
Project-URL: Documentation, https://docs.plexspaces.io
|
|
9
|
+
Project-URL: Repository, https://github.com/plexspaces/plexspaces
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Provides-Extra: proto
|
|
20
|
+
Requires-Dist: betterproto>=2.0.0b7; extra == "proto"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
24
|
+
Requires-Dist: componentize-py>=0.12; extra == "dev"
|
|
25
|
+
Requires-Dist: betterproto>=2.0.0b7; extra == "dev"
|
|
26
|
+
Requires-Dist: grpcio-tools>=1.50; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# PlexSpaces Python SDK
|
|
29
|
+
|
|
30
|
+
Build PlexSpaces actors with minimal boilerplate, inspired by [Ray's `@ray.remote`](https://docs.ray.io/en/latest/ray-core/api/doc/ray.remote.html).
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Activate virtualenv
|
|
36
|
+
source ~/venv/bin/activate
|
|
37
|
+
|
|
38
|
+
# From source (development)
|
|
39
|
+
cd sdks/python
|
|
40
|
+
pip install -e ".[dev]"
|
|
41
|
+
|
|
42
|
+
# Required for building WASM
|
|
43
|
+
pip install componentize-py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
### 1. Write Your Actor (No Boilerplate!)
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# bank_account.py
|
|
52
|
+
from plexspaces import actor, state, handler
|
|
53
|
+
|
|
54
|
+
@actor
|
|
55
|
+
class BankAccount:
|
|
56
|
+
balance: int = state(default=0)
|
|
57
|
+
account_id: str = state(default="")
|
|
58
|
+
|
|
59
|
+
@handler("deposit")
|
|
60
|
+
def deposit(self, amount: int) -> dict:
|
|
61
|
+
self.balance += amount
|
|
62
|
+
return {"balance": self.balance}
|
|
63
|
+
|
|
64
|
+
@handler("withdraw")
|
|
65
|
+
def withdraw(self, amount: int) -> dict:
|
|
66
|
+
if amount > self.balance:
|
|
67
|
+
return {"error": "insufficient_funds"}
|
|
68
|
+
self.balance -= amount
|
|
69
|
+
return {"balance": self.balance}
|
|
70
|
+
|
|
71
|
+
@handler("balance", "get")
|
|
72
|
+
def get_balance(self) -> dict:
|
|
73
|
+
return {"balance": self.balance}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Build to WASM
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
plexspaces-py build bank_account.py -o bank_account_actor.wasm
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Deploy
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl -X POST http://localhost:8094/api/v1/deploy \
|
|
86
|
+
-F "namespace=default" \
|
|
87
|
+
-F "actor_type=bank_account" \
|
|
88
|
+
-F "wasm=@bank_account_actor.wasm"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Proto-generated types (optional)
|
|
92
|
+
|
|
93
|
+
Workflow and common messages are defined in Protocol Buffers. After **`make proto`** or **`make proto-python`** at the repo root, typed classes appear under **`plexspaces/generated/`** (betterproto). The **`plexspaces.workflow`** module now treats the proto-shaped **`RetryConfig`** object as the canonical SDK model while still accepting dict input for compatibility in existing call sites.
|
|
94
|
+
|
|
95
|
+
Install **`pip install -e ".[dev]"`** or **`pip install -e ".[proto]"`** to use betterproto locally. Regeneration uses local buf plugins; see **`make proto-install-deps`** in the root `Makefile`.
|
|
96
|
+
|
|
97
|
+
## Features
|
|
98
|
+
|
|
99
|
+
### State Persistence
|
|
100
|
+
|
|
101
|
+
Fields decorated with `state()` are automatically:
|
|
102
|
+
- Serialized in `get_state()`
|
|
103
|
+
- Restored in `set_state()`
|
|
104
|
+
- Persisted across actor restarts
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
@actor
|
|
108
|
+
class Counter:
|
|
109
|
+
count: int = state(default=0) # Immutable default
|
|
110
|
+
history: list = state(default_factory=list) # Mutable default
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Message Handlers
|
|
114
|
+
|
|
115
|
+
Use `@handler()` to route messages to methods:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
@actor
|
|
119
|
+
class Calculator:
|
|
120
|
+
@handler("add")
|
|
121
|
+
def add(self, a: int, b: int) -> dict:
|
|
122
|
+
return {"result": a + b}
|
|
123
|
+
|
|
124
|
+
@handler("sub", "subtract") # Multiple message types
|
|
125
|
+
def subtract(self, a: int, b: int) -> dict:
|
|
126
|
+
return {"result": a - b}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Ask vs Tell (GET vs POST)
|
|
130
|
+
|
|
131
|
+
The HTTP gateway uses explicit actor-runtime endpoints: **GET** on `/api/v1/actors/{namespace}/{actor_type}` and **GET|POST|PUT** on `/ask` route to **AskReply** (request-reply), while **POST/PUT** on `/api/v1/actors/{namespace}/{actor_type}` route to **SendMessage** (fire-and-forget). Design handlers accordingly:
|
|
132
|
+
|
|
133
|
+
- **Read handlers** (e.g. `count`, `get_readings`): Use **GET** so the client receives the reply. Query params are passed as payload, e.g. `?msg_type=readings&limit=10` → `{"msg_type": "readings", "limit": "10"}`.
|
|
134
|
+
- **Write handlers** (e.g. `ingest`, `clear`): Use **POST** (tell); the client does not wait for a reply.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
@actor
|
|
138
|
+
class SensorStream:
|
|
139
|
+
readings: list = state(default_factory=list)
|
|
140
|
+
|
|
141
|
+
@handler("ingest") # POST (tell) - fire-and-forget
|
|
142
|
+
def ingest(self, sensor_id: str = "", value: str = "0") -> dict:
|
|
143
|
+
self.readings.append({"sensor_id": sensor_id, "value": value})
|
|
144
|
+
return {"status": "ok"}
|
|
145
|
+
|
|
146
|
+
@handler("count", "call") # GET (ask) - client gets reply
|
|
147
|
+
def count(self) -> dict:
|
|
148
|
+
return {"reading_count": len(self.readings)}
|
|
149
|
+
|
|
150
|
+
@handler("readings") # GET (ask) - e.g. ?msg_type=readings&limit=10
|
|
151
|
+
def get_readings(self, limit: int = None) -> dict:
|
|
152
|
+
n = int(limit) if limit is not None else 100
|
|
153
|
+
return {"readings": self.readings[-n:], "count": len(self.readings[-n:])}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### WIT/WASM compatibility (production-grade)
|
|
157
|
+
|
|
158
|
+
The SDK targets the **plexspaces-actor** WIT world: all data crosses the WASM boundary as **JSON strings** (no raw float/list/dict in WIT). To avoid traps in componentize-py:
|
|
159
|
+
|
|
160
|
+
1. **State and handler returns**: Use only JSON-serializable types: `str`, `int`, `bool`, `list`, `dict`. Avoid raw `float` in state or return values; use `str` for numbers if needed (the SDK sanitizes float→str at the boundary).
|
|
161
|
+
2. **No in-place mutation of state lists**: Prefer replacing the list instead of `list.append()` then `list.pop(0)` in a loop (e.g. `self.items = (list(self.items) + [new])[-max:]` or build a new list and assign).
|
|
162
|
+
3. **Clear/reset**: Use `self.items = []` instead of `self.items.clear()`.
|
|
163
|
+
4. **Return values**: Handlers should return plain dicts; the generated wrapper runs `_sanitize_payload_for_wasm` before `json.dumps` so floats become strings at the boundary.
|
|
164
|
+
|
|
165
|
+
The generated wrapper automatically sanitizes `get_state`, `set_state`, and `handle` payloads for the WASM boundary.
|
|
166
|
+
|
|
167
|
+
### Host Functions
|
|
168
|
+
|
|
169
|
+
Access PlexSpaces capabilities via `host`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from plexspaces import actor, handler, host
|
|
173
|
+
|
|
174
|
+
@actor
|
|
175
|
+
class ChatRoom:
|
|
176
|
+
@handler("send")
|
|
177
|
+
def send_message(self, text: str) -> dict:
|
|
178
|
+
# Log message
|
|
179
|
+
host.info(f"Sending: {text}")
|
|
180
|
+
|
|
181
|
+
# Broadcast to group
|
|
182
|
+
host.process_groups.publish("chat-room", {"text": text})
|
|
183
|
+
|
|
184
|
+
return {"status": "sent"}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## API Reference
|
|
188
|
+
|
|
189
|
+
### Decorators
|
|
190
|
+
|
|
191
|
+
| Decorator | Description |
|
|
192
|
+
|-----------|-------------|
|
|
193
|
+
| `@actor` | Define a PlexSpaces actor class (GenServer behavior) |
|
|
194
|
+
| `@actor(facets=["..."])` | Actor with facet declaration (e.g., `facets=["durability"]`) |
|
|
195
|
+
| `@event_actor` | Event-handler actor (GenEvent behavior) |
|
|
196
|
+
| `@fsm_actor` | Finite state machine actor (GenStateMachine behavior) |
|
|
197
|
+
| `@fsm_actor(states=[...], initial="...")` | FSM actor with explicit state list and initial state |
|
|
198
|
+
| `@gen_server_actor` | Explicit GenServer actor (same as `@actor`) |
|
|
199
|
+
| `@workflow_actor` | Workflow/orchestration actor |
|
|
200
|
+
| `@run_handler` | Workflow run entrypoint |
|
|
201
|
+
| `@signal_handler("name")` | Named workflow signal handler |
|
|
202
|
+
| `@query_handler("name")` | Named workflow query handler |
|
|
203
|
+
| `@handler(*msg_types)` | Route messages to this method |
|
|
204
|
+
| `state(default=None, default_factory=None)` | Define persistent state field |
|
|
205
|
+
| `@init_handler` | Custom initialization handler |
|
|
206
|
+
|
|
207
|
+
### Behavior Types
|
|
208
|
+
|
|
209
|
+
| Decorator | Behavior | Use Case | Pattern |
|
|
210
|
+
|-----------|----------|----------|---------|
|
|
211
|
+
| `@actor` | GenServer | Request-reply actors (default) | Auto `call` |
|
|
212
|
+
| `@gen_server_actor` | GenServer | Explicit GenServer | Auto `call` |
|
|
213
|
+
| `@event_actor` | GenEvent | Fire-and-forget event handlers | `cast` |
|
|
214
|
+
| `@fsm_actor` | GenStateMachine | State machine workflows | Auto `call` |
|
|
215
|
+
| `@fsm_actor(states=[...], initial="...")` | GenStateMachine | FSM with documented states | Auto `call` |
|
|
216
|
+
| `@workflow_actor` | Workflow | Long-running orchestrations | Auto `call` |
|
|
217
|
+
|
|
218
|
+
**GenServer Request-Reply Default**: When using `@actor` or `@gen_server_actor`, handlers default to the `call` invocation automatically. You do not need to specify that in `@handler()`.
|
|
219
|
+
|
|
220
|
+
### Facets
|
|
221
|
+
|
|
222
|
+
Facets declare what capabilities an actor expects. For WASM actors:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
@actor(facets=["durability"])
|
|
226
|
+
class DurableAccount:
|
|
227
|
+
balance: int = state(default=0)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
| Facet | WASM Behavior |
|
|
231
|
+
|-------|---------------|
|
|
232
|
+
| `durability` | Checkpoint-based persistence via `WasmConfig.durability_enabled` |
|
|
233
|
+
| `registry` | Service discovery via `RegistryFacet` in app-config |
|
|
234
|
+
|
|
235
|
+
**Note**: WASM durability uses checkpoint-based persistence (get_state/set_state), not the Rust `DurabilityFacet`. Enable via `durability_enabled: true` in release.yaml or WasmConfig.
|
|
236
|
+
|
|
237
|
+
### Host Functions (Capabilities)
|
|
238
|
+
|
|
239
|
+
PlexSpaces provides comprehensive host functions for WASM actors, following wasmCloud-style capability-based design.
|
|
240
|
+
|
|
241
|
+
#### Messaging
|
|
242
|
+
|
|
243
|
+
| Function | Description | Example |
|
|
244
|
+
|----------|-------------|---------|
|
|
245
|
+
| `host.send(to, msg_type, payload)` | Send message to another actor (fire-and-forget) | `host.send("actor-2", "ping", {"data": "hello"})` |
|
|
246
|
+
| `host.ask(to, msg_type, payload, timeout_ms)` | Send request and wait for response (request-reply) | `response = host.ask("actor-2", "get_data", {}, 5000)` |
|
|
247
|
+
| `host.self_id()` | Get own actor ID | `my_id = host.self_id()` |
|
|
248
|
+
|
|
249
|
+
#### Actor Lifecycle
|
|
250
|
+
|
|
251
|
+
| Function | Description | Example |
|
|
252
|
+
|----------|-------------|---------|
|
|
253
|
+
| `host.spawn(module_ref, actor_id, init_config_json)` | Spawn a new actor | `id = host.spawn("calculator", "calc-1", "{}")` |
|
|
254
|
+
| `host.stop(actor_id)` | Stop an actor gracefully | `host.stop("calc-1")` |
|
|
255
|
+
|
|
256
|
+
#### Actor Linking & Monitoring (Erlang/OTP patterns)
|
|
257
|
+
|
|
258
|
+
| Function | Description | Example |
|
|
259
|
+
|----------|-------------|---------|
|
|
260
|
+
| `host.link(actor_id)` | Bidirectional link (crash propagation) | `host.link("worker-1")` |
|
|
261
|
+
| `host.unlink(actor_id)` | Remove bidirectional link | `host.unlink("worker-1")` |
|
|
262
|
+
| `host.monitor(actor_id)` | Unidirectional monitor (exit notification) | `ref = host.monitor("worker-1")` |
|
|
263
|
+
| `host.demonitor(monitor_ref)` | Cancel monitor | `host.demonitor(ref)` |
|
|
264
|
+
|
|
265
|
+
#### Timers (Delayed Messaging)
|
|
266
|
+
|
|
267
|
+
| Function | Description | Example |
|
|
268
|
+
|----------|-------------|---------|
|
|
269
|
+
| `host.send_after(delay_ms, msg_type, payload)` | Send message to self after delay | `timer_id = host.send_after(5000, "cleanup", {})` |
|
|
270
|
+
|
|
271
|
+
#### Logging & Time
|
|
272
|
+
|
|
273
|
+
| Function | Description | Example |
|
|
274
|
+
|----------|-------------|---------|
|
|
275
|
+
| `host.log(level, message)` | Log a message | `host.log("info", "Processing")` |
|
|
276
|
+
| `host.info(message)` | Log info message | `host.info("Processing")` |
|
|
277
|
+
| `host.debug(message)` | Log debug message | `host.debug("Debug info")` |
|
|
278
|
+
| `host.warn(message)` | Log warning message | `host.warn("Warning")` |
|
|
279
|
+
| `host.error(message)` | Log error message | `host.error("Error occurred")` |
|
|
280
|
+
| `host.now_ms()` | Get current timestamp (ms) | `ts = host.now_ms()` |
|
|
281
|
+
|
|
282
|
+
#### Key-Value Storage
|
|
283
|
+
|
|
284
|
+
| Function | Description | Example |
|
|
285
|
+
|----------|-------------|---------|
|
|
286
|
+
| `host.kv_get(key)` | Get value for key | `value = host.kv_get("session:123")` |
|
|
287
|
+
| `host.kv_put(key, value)` | Store value for key | `host.kv_put("session:123", json.dumps(data))` |
|
|
288
|
+
| `host.kv_delete(key)` | Delete key | `host.kv_delete("session:123")` |
|
|
289
|
+
| `host.kv_list(prefix)` | List keys with prefix (returns JSON array) | `keys = json.loads(host.kv_list("session:"))` |
|
|
290
|
+
|
|
291
|
+
**Backends:** Redis, SQLite, DynamoDB, PostgreSQL, Memory
|
|
292
|
+
|
|
293
|
+
#### Blob Storage
|
|
294
|
+
|
|
295
|
+
| Function | Description | Example |
|
|
296
|
+
|----------|-------------|---------|
|
|
297
|
+
| `host.blob_upload(blob_id, data, content_type)` | Upload blob (data must be base64-encoded) | `host.blob_upload("file-1", base64_data, "image/png")` |
|
|
298
|
+
| `host.blob_download(blob_id)` | Download blob (returns base64) | `data = host.blob_download("file-1")` |
|
|
299
|
+
| `host.blob_delete(blob_id)` | Delete blob | `host.blob_delete("file-1")` |
|
|
300
|
+
| `host.blob_list(prefix)` | List blobs with prefix (returns JSON array) | `blobs = json.loads(host.blob_list("files/"))` |
|
|
301
|
+
|
|
302
|
+
**Backends:** S3, Azure Blob, GCP Storage, embedded object store (rustfs), Filesystem
|
|
303
|
+
|
|
304
|
+
#### ShardGroup / Application Metrics
|
|
305
|
+
|
|
306
|
+
| Function | Description | Example |
|
|
307
|
+
|----------|-------------|---------|
|
|
308
|
+
| `host.create_shard_group(request)` | Create a shard group using framework/proto field names | `group = host.create_shard_group({"group_id": "g1", "actor_type": "worker", "shard_count": 4})` |
|
|
309
|
+
| `host.bulk_update_shard_group(request)` | Bulk update shard members | `host.bulk_update_shard_group({"group_id": "g1", "updates": {"k1": {"op": "init"}}})` |
|
|
310
|
+
| `host.map_shard_group(request)` | Map a query across shards | `resp = host.map_shard_group({"group_id": "g1", "query": {"op": "stats"}})` |
|
|
311
|
+
| `host.scatter_gather(request)` | Scatter/gather across shards | `resp = host.scatter_gather({"group_id": "g1", "query": {"op": "compute"}, "aggregation": "concat"})` |
|
|
312
|
+
| `host.application_metrics_add(application_id, metrics)` | Merge node-local application metrics delta | `host.application_metrics_add("heat-app", {"counter_metrics": {"worker_messages": 1}})` |
|
|
313
|
+
| `host.application_get_status(application_id, node_id)` | Get per-node application status and metrics | `status = host.application_get_status("heat-app", "test-node-8093")` |
|
|
314
|
+
|
|
315
|
+
These methods use the same actor-world WIT host surface as Rust WASM apps. The SDK wrapper is
|
|
316
|
+
thin: it serializes request/response JSON using the framework field names and delegates to the
|
|
317
|
+
underlying framework `ActorService` and application manager.
|
|
318
|
+
|
|
319
|
+
#### TupleSpace (Linda-style Coordination)
|
|
320
|
+
|
|
321
|
+
**Preferred: `host.ts`** — list-in, list-out; use `None` in patterns for wildcards.
|
|
322
|
+
|
|
323
|
+
| Function | Description | Example |
|
|
324
|
+
|----------|-------------|---------|
|
|
325
|
+
| `host.ts.write(tuple_list)` | Write tuple (list of JSON-serializable elements) | `host.ts.write(["task", "worker-1", 123])` |
|
|
326
|
+
| `host.ts.read(pattern)` | Read one match (non-destructive). Returns list or None. | `t = host.ts.read(["task", None, None])` |
|
|
327
|
+
| `host.ts.take(pattern)` | Take one match (destructive). Returns list or None. | `t = host.ts.take(["task", None, None]); if t: ...` |
|
|
328
|
+
| `host.ts.read_all(pattern)` | Read all matches (non-destructive). Returns list of lists. | `all_ = host.ts.read_all(["task", None, None])` |
|
|
329
|
+
|
|
330
|
+
Low-level string API (when you need raw JSON): `host.ts_write(tuple_json)`, `host.ts_read(pattern_json)`, `host.ts_take(pattern_json)`, `host.ts_read_all(pattern_json)`.
|
|
331
|
+
|
|
332
|
+
#### Distributed Locks
|
|
333
|
+
|
|
334
|
+
| Function | Description | Example |
|
|
335
|
+
|----------|-------------|---------|
|
|
336
|
+
| `host.lock_acquire(tenant_id, namespace, holder_id, lock_name, lease_duration_secs, timeout_ms)` | Acquire lock (returns JSON with version) | `lock = json.loads(host.lock_acquire("tenant", "ns", "actor-1", "resource", 30, 5000))` |
|
|
337
|
+
| `host.lock_renew(lock_id, tenant_id, namespace, holder_id, lock_version, lease_duration_secs)` | Renew lease on held lock | `new_version = host.lock_renew("resource", "tenant", "ns", "actor-1", version, 30)` |
|
|
338
|
+
| `host.lock_release(lock_id, tenant_id, namespace, holder_id, lock_version)` | Release lock | `host.lock_release("resource", "tenant", "ns", "actor-1", version)` |
|
|
339
|
+
|
|
340
|
+
#### Process Groups (Pub/Sub)
|
|
341
|
+
|
|
342
|
+
| Function | Description | Example |
|
|
343
|
+
|----------|-------------|---------|
|
|
344
|
+
| `host.process_groups.join(group)` | Join a process group | `host.process_groups.join("chat-room")` |
|
|
345
|
+
| `host.process_groups.leave(group)` | Leave a process group | `host.process_groups.leave("chat-room")` |
|
|
346
|
+
| `host.process_groups.members(group)` | Get group members (returns list) | `members = host.process_groups.members("chat-room")` |
|
|
347
|
+
| `host.process_groups.broadcast(group, msg_type, payload)` | Broadcast to all group members; msg_type is used for routing so payload can be data-only. | `host.process_groups.broadcast("chat-room", "message", {"text": "hello"})` |
|
|
348
|
+
|
|
349
|
+
### Capability Comparison: PlexSpaces vs wasmCloud
|
|
350
|
+
|
|
351
|
+
| Capability | wasmCloud | PlexSpaces | Status |
|
|
352
|
+
|------------|-----------|------------|--------|
|
|
353
|
+
| **HTTP Client** | ✅ `wasi:http` | ⚠️ Simulated via `host.ask()` | Partial |
|
|
354
|
+
| **HTTP Server** | ✅ `wasi:http` | ❌ Not available | Missing |
|
|
355
|
+
| **KeyValue Storage** | ✅ `wasi:keyvalue` | ✅ `host.kv_*` | ✅ Complete |
|
|
356
|
+
| **Blob Storage** | ✅ `wasi:blobstore` | ✅ `host.blob_*` | ✅ Complete |
|
|
357
|
+
| **Messaging** | ✅ `wasmcloud:messaging` | ✅ `host.send/ask` | ✅ Complete |
|
|
358
|
+
| **Logging** | ✅ `wasi:logging` | ✅ `host.log` | ✅ Complete |
|
|
359
|
+
| **Process Groups** | ❌ Not available | ✅ `host.pg_*` | ✅ Extra |
|
|
360
|
+
| **Elastic pool** | ❌ Not available | ✅ `host.pool_checkout` / `host.pool_checkin` / `host.pool_get_metrics` | ✅ Extra |
|
|
361
|
+
| **TupleSpace** | ❌ Not available | ✅ `host.ts` / `host.ts_*` | ✅ Extra |
|
|
362
|
+
| **Distributed Locks** | ❌ Not available | ✅ `host.lock_*` | ✅ Extra |
|
|
363
|
+
| **Timers** | ❌ Not available | ✅ `host.send_after()` | ✅ Extra |
|
|
364
|
+
|
|
365
|
+
**Note:** HTTP Client can be simulated by calling HTTP gateway actors via `host.ask()`. See [Session Store example](../../examples/python/apps/migrating_wasmcloud/README.md) for details.
|
|
366
|
+
|
|
367
|
+
## Migration from Legacy Examples
|
|
368
|
+
|
|
369
|
+
If you have existing actors using the WIT interface directly:
|
|
370
|
+
|
|
371
|
+
### Before (Legacy)
|
|
372
|
+
```python
|
|
373
|
+
from wit_world import exports
|
|
374
|
+
import json
|
|
375
|
+
|
|
376
|
+
class Actor(exports.Actor):
|
|
377
|
+
def init(self, config_json: str) -> str:
|
|
378
|
+
global _balance
|
|
379
|
+
_balance = 0
|
|
380
|
+
return ""
|
|
381
|
+
|
|
382
|
+
def handle(self, from_actor: str, msg_type: str, payload_json: str) -> str:
|
|
383
|
+
global _balance
|
|
384
|
+
data = json.loads(payload_json)
|
|
385
|
+
if msg_type == "deposit":
|
|
386
|
+
_balance += data["amount"]
|
|
387
|
+
return json.dumps({"balance": _balance})
|
|
388
|
+
# ... lots of boilerplate
|
|
389
|
+
|
|
390
|
+
def get_state(self) -> str:
|
|
391
|
+
return json.dumps({"balance": _balance})
|
|
392
|
+
|
|
393
|
+
def set_state(self, state_json: str) -> str:
|
|
394
|
+
global _balance
|
|
395
|
+
_balance = json.loads(state_json)["balance"]
|
|
396
|
+
return ""
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### After (SDK)
|
|
400
|
+
```python
|
|
401
|
+
from plexspaces import actor, state, handler
|
|
402
|
+
|
|
403
|
+
@actor
|
|
404
|
+
class BankAccount:
|
|
405
|
+
balance: int = state(default=0)
|
|
406
|
+
|
|
407
|
+
@handler("deposit")
|
|
408
|
+
def deposit(self, amount: int) -> dict:
|
|
409
|
+
self.balance += amount
|
|
410
|
+
return {"balance": self.balance}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Tier 1 Ergonomics Helpers
|
|
414
|
+
|
|
415
|
+
Convenience wrappers available on the `host` singleton and via `EventLog` from `plexspaces.host`.
|
|
416
|
+
|
|
417
|
+
### Process Groups
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
from plexspaces import host
|
|
421
|
+
|
|
422
|
+
# First member of a group (None if empty)
|
|
423
|
+
router = host.process_groups.first("svc:llm_router")
|
|
424
|
+
|
|
425
|
+
# Raises RuntimeError if empty
|
|
426
|
+
router = host.process_groups.first_or_raise("svc:llm_router")
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### KV JSON Helpers
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
# Write a JSON-serializable value
|
|
433
|
+
host.kv_put_json("task:1", {"seq": 1, "kind": "summarize"})
|
|
434
|
+
|
|
435
|
+
# Read (returns None if missing or corrupt JSON)
|
|
436
|
+
task = host.kv_get_json("task:1")
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Metrics Helpers
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
# Increment one counter by 1 (errors swallowed)
|
|
443
|
+
host.incr_counter("my-app", "requests_processed")
|
|
444
|
+
|
|
445
|
+
# Increment multiple counters
|
|
446
|
+
host.incr_counters("my-app", {"cache_hits": 5, "cache_misses": 2})
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### EventLog
|
|
450
|
+
|
|
451
|
+
Monotonic append-only log backed by KV. Embed in actor state (JSON-serializable via `state()`). Multiple consumers track independent read cursors.
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
from plexspaces.host import EventLog
|
|
455
|
+
from plexspaces import actor, state, handler, host
|
|
456
|
+
|
|
457
|
+
@actor
|
|
458
|
+
class AuditActor:
|
|
459
|
+
log: EventLog = state(default_factory=EventLog)
|
|
460
|
+
|
|
461
|
+
@handler("record")
|
|
462
|
+
def record(self, event: dict) -> dict:
|
|
463
|
+
seq = self.log.append(host, "audit:", event)
|
|
464
|
+
return {"seq": seq}
|
|
465
|
+
|
|
466
|
+
@handler("poll")
|
|
467
|
+
def poll(self, consumer_id: str, limit: int = 50) -> dict:
|
|
468
|
+
events, cursor = self.log.poll(host, "audit:", consumer_id, limit=limit)
|
|
469
|
+
return {"events": events, "cursor": cursor}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Development
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
# Activate virtualenv (use project or shared venv)
|
|
476
|
+
source ~/venv/bin/activate
|
|
477
|
+
|
|
478
|
+
# Install in editable mode with dev deps
|
|
479
|
+
pip install -e ".[dev]"
|
|
480
|
+
|
|
481
|
+
# Run tests (this directory)
|
|
482
|
+
pytest
|
|
483
|
+
|
|
484
|
+
# Or from repository root: make test runs pytest here when VENV_PATH (default ~/venv) has pytest
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
# Build examples
|
|
489
|
+
cd examples
|
|
490
|
+
plexspaces-py build chat_room.py -v
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Comparison with Other Frameworks
|
|
494
|
+
|
|
495
|
+
| Feature | PlexSpaces SDK | Ray | Temporal |
|
|
496
|
+
|---------|---------------|-----|----------|
|
|
497
|
+
| Decorator | `@actor` | `@ray.remote` | `@workflow.defn` |
|
|
498
|
+
| State | `state()` | Class attrs | N/A |
|
|
499
|
+
| Handlers | `@handler()` | Methods | `@activity.defn` |
|
|
500
|
+
| Build | WASM | Python | Docker |
|
|
501
|
+
| Runtime | PlexSpaces | Ray Cluster | Temporal Server |
|
|
502
|
+
|
|
503
|
+
## License
|
|
504
|
+
|
|
505
|
+
AGPL-3.0-or-later
|