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.
Files changed (103) hide show
  1. plexspaces-0.1.1/PKG-INFO +505 -0
  2. plexspaces-0.1.1/README.md +478 -0
  3. plexspaces-0.1.1/plexspaces/__init__.py +80 -0
  4. plexspaces-0.1.1/plexspaces/actor_id.py +114 -0
  5. plexspaces-0.1.1/plexspaces/decorators.py +691 -0
  6. plexspaces-0.1.1/plexspaces/generated/__init__.py +0 -0
  7. plexspaces-0.1.1/plexspaces/generated/buf/__init__.py +0 -0
  8. plexspaces-0.1.1/plexspaces/generated/buf/validate/__init__.py +3799 -0
  9. plexspaces-0.1.1/plexspaces/generated/google/__init__.py +0 -0
  10. plexspaces-0.1.1/plexspaces/generated/google/api/__init__.py +379 -0
  11. plexspaces-0.1.1/plexspaces/generated/grpc/__init__.py +0 -0
  12. plexspaces-0.1.1/plexspaces/generated/grpc/gateway/__init__.py +0 -0
  13. plexspaces-0.1.1/plexspaces/generated/grpc/gateway/protoc_gen_openapiv2/__init__.py +0 -0
  14. plexspaces-0.1.1/plexspaces/generated/grpc/gateway/protoc_gen_openapiv2/options/__init__.py +1022 -0
  15. plexspaces-0.1.1/plexspaces/generated/plexspaces/__init__.py +0 -0
  16. plexspaces-0.1.1/plexspaces/generated/plexspaces/actor/__init__.py +0 -0
  17. plexspaces-0.1.1/plexspaces/generated/plexspaces/actor/v1/__init__.py +3538 -0
  18. plexspaces-0.1.1/plexspaces/generated/plexspaces/application/__init__.py +0 -0
  19. plexspaces-0.1.1/plexspaces/generated/plexspaces/application/v1/__init__.py +827 -0
  20. plexspaces-0.1.1/plexspaces/generated/plexspaces/behaviors/__init__.py +0 -0
  21. plexspaces-0.1.1/plexspaces/generated/plexspaces/behaviors/v1/__init__.py +63 -0
  22. plexspaces-0.1.1/plexspaces/generated/plexspaces/channel/__init__.py +0 -0
  23. plexspaces-0.1.1/plexspaces/generated/plexspaces/channel/v1/__init__.py +915 -0
  24. plexspaces-0.1.1/plexspaces/generated/plexspaces/circuitbreaker/__init__.py +0 -0
  25. plexspaces-0.1.1/plexspaces/generated/plexspaces/circuitbreaker/prv/__init__.py +398 -0
  26. plexspaces-0.1.1/plexspaces/generated/plexspaces/common/__init__.py +0 -0
  27. plexspaces-0.1.1/plexspaces/generated/plexspaces/common/v1/__init__.py +691 -0
  28. plexspaces-0.1.1/plexspaces/generated/plexspaces/config/__init__.py +0 -0
  29. plexspaces-0.1.1/plexspaces/generated/plexspaces/config/v1/__init__.py +109 -0
  30. plexspaces-0.1.1/plexspaces/generated/plexspaces/dashboard/__init__.py +0 -0
  31. plexspaces-0.1.1/plexspaces/generated/plexspaces/dashboard/v1/__init__.py +647 -0
  32. plexspaces-0.1.1/plexspaces/generated/plexspaces/facets/__init__.py +0 -0
  33. plexspaces-0.1.1/plexspaces/generated/plexspaces/facets/v1/__init__.py +333 -0
  34. plexspaces-0.1.1/plexspaces/generated/plexspaces/firecracker/__init__.py +0 -0
  35. plexspaces-0.1.1/plexspaces/generated/plexspaces/firecracker/v1/__init__.py +890 -0
  36. plexspaces-0.1.1/plexspaces/generated/plexspaces/grpc/__init__.py +0 -0
  37. plexspaces-0.1.1/plexspaces/generated/plexspaces/grpc/v1/__init__.py +679 -0
  38. plexspaces-0.1.1/plexspaces/generated/plexspaces/journaling/__init__.py +0 -0
  39. plexspaces-0.1.1/plexspaces/generated/plexspaces/journaling/v1/__init__.py +1006 -0
  40. plexspaces-0.1.1/plexspaces/generated/plexspaces/keyvalue/__init__.py +0 -0
  41. plexspaces-0.1.1/plexspaces/generated/plexspaces/keyvalue/v1/__init__.py +1007 -0
  42. plexspaces-0.1.1/plexspaces/generated/plexspaces/locks/__init__.py +0 -0
  43. plexspaces-0.1.1/plexspaces/generated/plexspaces/locks/prv/__init__.py +122 -0
  44. plexspaces-0.1.1/plexspaces/generated/plexspaces/mailbox/__init__.py +0 -0
  45. plexspaces-0.1.1/plexspaces/generated/plexspaces/mailbox/v1/__init__.py +246 -0
  46. plexspaces-0.1.1/plexspaces/generated/plexspaces/metrics/__init__.py +0 -0
  47. plexspaces-0.1.1/plexspaces/generated/plexspaces/metrics/v1/__init__.py +878 -0
  48. plexspaces-0.1.1/plexspaces/generated/plexspaces/node/__init__.py +0 -0
  49. plexspaces-0.1.1/plexspaces/generated/plexspaces/node/v1/__init__.py +2564 -0
  50. plexspaces-0.1.1/plexspaces/generated/plexspaces/object_registry/__init__.py +0 -0
  51. plexspaces-0.1.1/plexspaces/generated/plexspaces/object_registry/v1/__init__.py +759 -0
  52. plexspaces-0.1.1/plexspaces/generated/plexspaces/persistence/__init__.py +0 -0
  53. plexspaces-0.1.1/plexspaces/generated/plexspaces/persistence/prv/__init__.py +539 -0
  54. plexspaces-0.1.1/plexspaces/generated/plexspaces/pool/__init__.py +0 -0
  55. plexspaces-0.1.1/plexspaces/generated/plexspaces/pool/v1/__init__.py +791 -0
  56. plexspaces-0.1.1/plexspaces/generated/plexspaces/processgroups/__init__.py +0 -0
  57. plexspaces-0.1.1/plexspaces/generated/plexspaces/processgroups/v1/__init__.py +622 -0
  58. plexspaces-0.1.1/plexspaces/generated/plexspaces/scheduling/__init__.py +0 -0
  59. plexspaces-0.1.1/plexspaces/generated/plexspaces/scheduling/v1/__init__.py +364 -0
  60. plexspaces-0.1.1/plexspaces/generated/plexspaces/security/__init__.py +0 -0
  61. plexspaces-0.1.1/plexspaces/generated/plexspaces/security/v1/__init__.py +254 -0
  62. plexspaces-0.1.1/plexspaces/generated/plexspaces/services/__init__.py +0 -0
  63. plexspaces-0.1.1/plexspaces/generated/plexspaces/services/prv/__init__.py +112 -0
  64. plexspaces-0.1.1/plexspaces/generated/plexspaces/storage/__init__.py +0 -0
  65. plexspaces-0.1.1/plexspaces/generated/plexspaces/storage/v1/__init__.py +747 -0
  66. plexspaces-0.1.1/plexspaces/generated/plexspaces/supervision/__init__.py +0 -0
  67. plexspaces-0.1.1/plexspaces/generated/plexspaces/supervision/v1/__init__.py +536 -0
  68. plexspaces-0.1.1/plexspaces/generated/plexspaces/system/__init__.py +0 -0
  69. plexspaces-0.1.1/plexspaces/generated/plexspaces/system/v1/__init__.py +1602 -0
  70. plexspaces-0.1.1/plexspaces/generated/plexspaces/timer/__init__.py +0 -0
  71. plexspaces-0.1.1/plexspaces/generated/plexspaces/timer/v1/__init__.py +402 -0
  72. plexspaces-0.1.1/plexspaces/generated/plexspaces/tuplespace/__init__.py +0 -0
  73. plexspaces-0.1.1/plexspaces/generated/plexspaces/tuplespace/v1/__init__.py +869 -0
  74. plexspaces-0.1.1/plexspaces/generated/plexspaces/wasm/__init__.py +0 -0
  75. plexspaces-0.1.1/plexspaces/generated/plexspaces/wasm/v1/__init__.py +920 -0
  76. plexspaces-0.1.1/plexspaces/generated/plexspaces/workflow/__init__.py +0 -0
  77. plexspaces-0.1.1/plexspaces/generated/plexspaces/workflow/v1/__init__.py +870 -0
  78. plexspaces-0.1.1/plexspaces/host.py +2093 -0
  79. plexspaces-0.1.1/plexspaces/leader_worker.py +148 -0
  80. plexspaces-0.1.1/plexspaces/proto_wire.py +1357 -0
  81. plexspaces-0.1.1/plexspaces/runtime.py +284 -0
  82. plexspaces-0.1.1/plexspaces/workflow.py +105 -0
  83. plexspaces-0.1.1/plexspaces.egg-info/PKG-INFO +505 -0
  84. plexspaces-0.1.1/plexspaces.egg-info/SOURCES.txt +101 -0
  85. plexspaces-0.1.1/plexspaces.egg-info/dependency_links.txt +1 -0
  86. plexspaces-0.1.1/plexspaces.egg-info/entry_points.txt +2 -0
  87. plexspaces-0.1.1/plexspaces.egg-info/requires.txt +10 -0
  88. plexspaces-0.1.1/plexspaces.egg-info/top_level.txt +2 -0
  89. plexspaces-0.1.1/plexspaces_cli/__init__.py +8 -0
  90. plexspaces-0.1.1/plexspaces_cli/build.py +374 -0
  91. plexspaces-0.1.1/pyproject.toml +56 -0
  92. plexspaces-0.1.1/setup.cfg +4 -0
  93. plexspaces-0.1.1/tests/test_abstractions_contract.py +166 -0
  94. plexspaces-0.1.1/tests/test_actor_id.py +78 -0
  95. plexspaces-0.1.1/tests/test_application_metrics_host.py +37 -0
  96. plexspaces-0.1.1/tests/test_decorators.py +680 -0
  97. plexspaces-0.1.1/tests/test_host.py +498 -0
  98. plexspaces-0.1.1/tests/test_host_collective.py +180 -0
  99. plexspaces-0.1.1/tests/test_host_tuple_space.py +106 -0
  100. plexspaces-0.1.1/tests/test_http_fetch_wire.py +31 -0
  101. plexspaces-0.1.1/tests/test_lock_wire.py +75 -0
  102. plexspaces-0.1.1/tests/test_runtime.py +154 -0
  103. 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