xstate-statemachine 0.1.0__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.
- xstate_statemachine-0.1.0/LICENSE +21 -0
- xstate_statemachine-0.1.0/PKG-INFO +380 -0
- xstate_statemachine-0.1.0/README.md +363 -0
- xstate_statemachine-0.1.0/pyproject.toml +26 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/__init__.py +31 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/events.py +39 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/exceptions.py +43 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/factory.py +46 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/interpreter.py +1492 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/logger.py +21 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/machine_logic.py +136 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/models.py +694 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/plugins.py +181 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/resolver.py +118 -0
- xstate_statemachine-0.1.0/src/xstate_statemachine/task_manager.py +85 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Basil T T
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: xstate-statemachine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A robust Python library for parsing and running XState JSON state machines.
|
|
5
|
+
Author: Basil T T
|
|
6
|
+
Author-email: tt.basil@gmail.com
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Project-URL: Bug Tracker, https://github.com/basiltt/xstate-statemachine/issues
|
|
14
|
+
Project-URL: Homepage, https://github.com/basiltt/xstate-statemachine
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# 🚦 XState StateMachine for Python
|
|
18
|
+
|
|
19
|
+
A robust, asynchronous, and feature-complete Python library for parsing and executing state machines defined in XState-compatible JSON.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
This library brings the power and clarity of formal state machines and statecharts, as popularized by XState, to the Python ecosystem. It allows you to define complex application logic as a clear, traversable graph and execute it in a fully asynchronous, predictable, and debuggable way.
|
|
24
|
+
|
|
25
|
+
Define your logic once in a simple JSON format, and use this library to bring it to life in your Python application.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🧭 Core Philosophy: Definition vs. Implementation
|
|
30
|
+
|
|
31
|
+
**Definition (The "What")**: You define your state machine's structure, states, and transitions in a JSON file. This is your blueprint. It describes what can happen.
|
|
32
|
+
|
|
33
|
+
**Implementation (The "How")**: You write the business logic—the actual code that runs—in a Python `MachineLogic` object. This describes how actions are performed or services are called.
|
|
34
|
+
|
|
35
|
+
This separation makes your application logic easier to understand, test, and maintain.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🎨 Design Your Logic Visually with the Stately Editor
|
|
40
|
+
|
|
41
|
+
One of the biggest advantages of using an XState-compatible format is the ability to visualize, design, and even simulate your logic using a graphical interface. The official Stately Editor allows you to drag-and-drop states, define transitions, and export the resulting JSON directly for use with this library.
|
|
42
|
+
|
|
43
|
+
**Start designing at the [Stately Editor](https://stately.ai/editor) →**
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ✨ Key Features
|
|
48
|
+
|
|
49
|
+
- **XState Compatible**: Parses JSON configurations generated from the XState ecosystem.
|
|
50
|
+
- **Fully Asynchronous**: Built on `asyncio` for modern, non-blocking applications.
|
|
51
|
+
- **Hierarchical & Parallel States**: Model complex logic with nested and parallel states.
|
|
52
|
+
- **Timed Events**: Use `after` for declarative, time-based transitions.
|
|
53
|
+
- **Asynchronous Services**: Use `invoke` to call async functions and react to their success (`onDone`) or failure (`onError`).
|
|
54
|
+
- **Actor Model**: Spawn child state machines from a parent machine for concurrent, isolated logic.
|
|
55
|
+
- **Guards**: Implement conditional transitions with simple guard functions.
|
|
56
|
+
- **Developer Friendly**: Full type hinting and a `LoggingInspector` plugin for easy debugging.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 📦 Installation
|
|
61
|
+
|
|
62
|
+
Install the library directly from PyPI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install xstate-statemachine
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🚀 Getting Started: A Simple Example
|
|
71
|
+
|
|
72
|
+
Let's create a simple toggle switch.
|
|
73
|
+
|
|
74
|
+
### 1. Define the Logic (`toggle.json`)
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"id": "toggle",
|
|
79
|
+
"initial": "inactive",
|
|
80
|
+
"states": {
|
|
81
|
+
"inactive": {
|
|
82
|
+
"on": {
|
|
83
|
+
"TOGGLE": "active"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"active": {
|
|
87
|
+
"on": {
|
|
88
|
+
"TOGGLE": "inactive"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Implement and Run (`main.py`)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
import json
|
|
100
|
+
from xstate_statemachine import create_machine, Interpreter
|
|
101
|
+
|
|
102
|
+
async def main():
|
|
103
|
+
# Load the machine definition from the JSON file
|
|
104
|
+
with open("toggle.json") as f:
|
|
105
|
+
toggle_config = json.load(f)
|
|
106
|
+
|
|
107
|
+
# Create a machine instance from the config
|
|
108
|
+
toggle_machine = create_machine(toggle_config)
|
|
109
|
+
|
|
110
|
+
# Create an interpreter to run the machine
|
|
111
|
+
interpreter = await Interpreter(toggle_machine).start()
|
|
112
|
+
print(f"Initial state: {interpreter.current_state_ids}")
|
|
113
|
+
|
|
114
|
+
# Send an event to the machine
|
|
115
|
+
print("Sending TOGGLE event...")
|
|
116
|
+
await interpreter.send("TOGGLE")
|
|
117
|
+
|
|
118
|
+
# Give the event loop a moment to process
|
|
119
|
+
await asyncio.sleep(0.01)
|
|
120
|
+
print(f"New state: {interpreter.current_state_ids}")
|
|
121
|
+
|
|
122
|
+
await interpreter.stop()
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
asyncio.run(main())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 3. See the Output
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Initial state: {'toggle.inactive'}
|
|
132
|
+
Sending TOGGLE event...
|
|
133
|
+
New state: {'toggle.active'}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 🧠 Core Concepts
|
|
139
|
+
|
|
140
|
+
### Actions & Context
|
|
141
|
+
|
|
142
|
+
Actions are "fire-and-forget" functions executed during a transition. They are the primary way to interact with the outside world or update the machine's internal context.
|
|
143
|
+
|
|
144
|
+
#### Example: drone.json
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"id": "drone",
|
|
149
|
+
"initial": "flying",
|
|
150
|
+
"context": { "battery": 100 },
|
|
151
|
+
"states": {
|
|
152
|
+
"flying": {
|
|
153
|
+
"on": {
|
|
154
|
+
"PHOTO_TAKEN": { "actions": ["decrementBattery"] }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### drone.py
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from xstate_statemachine import MachineLogic
|
|
165
|
+
|
|
166
|
+
def decrement_battery(interpreter, context, event, action_def):
|
|
167
|
+
context["battery"] -= 1
|
|
168
|
+
print(f"Battery at {context['battery']}%")
|
|
169
|
+
|
|
170
|
+
logic = MachineLogic(
|
|
171
|
+
actions={"decrementBattery": decrement_battery}
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### Guards
|
|
178
|
+
|
|
179
|
+
Guards are conditional checks that determine if a transition should be taken. If a guard returns `False`, the transition is blocked.
|
|
180
|
+
|
|
181
|
+
#### checkout.json
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"id": "cart",
|
|
186
|
+
"context": { "items": [] },
|
|
187
|
+
"on": {
|
|
188
|
+
"CHECKOUT": {
|
|
189
|
+
"target": "paying",
|
|
190
|
+
"guard": "cartIsNotEmpty"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### checkout.py
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from xstate_statemachine import MachineLogic
|
|
200
|
+
|
|
201
|
+
def cart_is_not_empty(context, event):
|
|
202
|
+
return len(context.get("items", [])) > 0
|
|
203
|
+
|
|
204
|
+
logic = MachineLogic(
|
|
205
|
+
guards={"cartIsNotEmpty": cart_is_not_empty}
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### Asynchronous Services (`invoke`)
|
|
212
|
+
|
|
213
|
+
For long-running or async operations, use `invoke`. The machine will transition to different states based on the success (`onDone`) or failure (`onError`) of the invoked async function.
|
|
214
|
+
|
|
215
|
+
#### fetch.json
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"id": "fetcher",
|
|
220
|
+
"initial": "loading",
|
|
221
|
+
"states": {
|
|
222
|
+
"loading": {
|
|
223
|
+
"invoke": {
|
|
224
|
+
"src": "fetchUserData",
|
|
225
|
+
"onDone": { "target": "success" },
|
|
226
|
+
"onError": { "target": "failure" }
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
"success": { "type": "final" },
|
|
230
|
+
"failure": { "type": "final" }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### fetch.py
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
import aiohttp
|
|
239
|
+
from xstate_statemachine import MachineLogic
|
|
240
|
+
|
|
241
|
+
async def fetch_user_data(interpreter, context, event):
|
|
242
|
+
async with aiohttp.ClientSession() as session:
|
|
243
|
+
async with session.get("https://api.example.com/user") as resp:
|
|
244
|
+
resp.raise_for_status()
|
|
245
|
+
return await resp.json()
|
|
246
|
+
|
|
247
|
+
logic = MachineLogic(
|
|
248
|
+
services={"fetchUserData": fetch_user_data}
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### Timed Events (`after`)
|
|
255
|
+
|
|
256
|
+
Declaratively schedule transitions to occur after a certain amount of time (in milliseconds).
|
|
257
|
+
|
|
258
|
+
#### traffic_light.json
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"id": "light",
|
|
263
|
+
"initial": "green",
|
|
264
|
+
"states": {
|
|
265
|
+
"green": { "after": { "30000": "yellow" } },
|
|
266
|
+
"yellow": { "after": { "5000": "red" } }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### Parallel States
|
|
274
|
+
|
|
275
|
+
Model system components that operate independently at the same time. The machine is in all child states of a parallel state simultaneously. The parent `onDone` transition only fires when all child regions have reached their final state.
|
|
276
|
+
|
|
277
|
+
#### build.json
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"id": "build",
|
|
282
|
+
"initial": "running",
|
|
283
|
+
"states": {
|
|
284
|
+
"running": {
|
|
285
|
+
"type": "parallel",
|
|
286
|
+
"onDone": "success",
|
|
287
|
+
"states": {
|
|
288
|
+
"backend": {
|
|
289
|
+
"initial": "compiling",
|
|
290
|
+
"states": {
|
|
291
|
+
"compiling": { "after": { "5000": "done" } },
|
|
292
|
+
"done": { "type": "final" }
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
"frontend": {
|
|
296
|
+
"initial": "linting",
|
|
297
|
+
"states": {
|
|
298
|
+
"linting": { "after": { "3000": "done" } },
|
|
299
|
+
"done": { "type": "final" }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
"success": { "type": "final" }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
### Actors (Spawning Machines)
|
|
312
|
+
|
|
313
|
+
For truly isolated, concurrent logic, you can spawn a child machine from a parent. The parent and child can communicate by sending events to each other.
|
|
314
|
+
|
|
315
|
+
To spawn an actor, define an entry action with the name `spawn_<serviceName>`, where `<serviceName>` corresponds to a key in your services logic that provides a `MachineNode`.
|
|
316
|
+
|
|
317
|
+
#### main_machine.py
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
import asyncio
|
|
321
|
+
from xstate_statemachine import create_machine, MachineLogic
|
|
322
|
+
|
|
323
|
+
# Define the child machine that will be spawned
|
|
324
|
+
child_config = { "id": "pinger", "on": { "PING": { "actions": ["pong"] } } }
|
|
325
|
+
child_logic = MachineLogic(
|
|
326
|
+
actions={"pong": lambda i,c,e,a: asyncio.create_task(i.parent.send("PONG"))}
|
|
327
|
+
)
|
|
328
|
+
child_machine_node = create_machine(child_config, child_logic)
|
|
329
|
+
|
|
330
|
+
# Define the parent machine
|
|
331
|
+
parent_config = {
|
|
332
|
+
"id": "parent",
|
|
333
|
+
"initial": "running",
|
|
334
|
+
"states": {
|
|
335
|
+
"running": {
|
|
336
|
+
"entry": ["spawn_pingerService"]
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
"on": { "PONG": "finished" }
|
|
340
|
+
}
|
|
341
|
+
parent_logic = MachineLogic(
|
|
342
|
+
services={"pingerService": child_machine_node}
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# When run, the spawned actor will be available in the parent's context:
|
|
346
|
+
# interpreter.context['actors'][actor_id].send("PING")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 🐞 Debugging with Plugins
|
|
352
|
+
|
|
353
|
+
The interpreter supports a plugin system to hook into its lifecycle. A built-in `LoggingInspector` is provided for easy, detailed debugging.
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
import logging
|
|
357
|
+
from xstate_statemachine import Interpreter, LoggingInspector
|
|
358
|
+
|
|
359
|
+
logging.basicConfig(level=logging.INFO)
|
|
360
|
+
|
|
361
|
+
interpreter = Interpreter(my_machine)
|
|
362
|
+
interpreter.use(LoggingInspector())
|
|
363
|
+
|
|
364
|
+
await interpreter.start()
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Now, all events, transitions, and actions will be logged to the console.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 🤝 Contributing
|
|
372
|
+
|
|
373
|
+
Contributions are welcome! If you find a bug or have a feature request, please open an issue on our [GitHub Issue Tracker](https://github.com/basiltt/xstate-statemachine/issues).
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 📄 License
|
|
378
|
+
|
|
379
|
+
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
380
|
+
|