pygubernator 0.0.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.
- pygubernator-0.0.1/LICENSE +21 -0
- pygubernator-0.0.1/PKG-INFO +394 -0
- pygubernator-0.0.1/README.md +355 -0
- pygubernator-0.0.1/pyproject.toml +130 -0
- pygubernator-0.0.1/setup.cfg +4 -0
- pygubernator-0.0.1/src/pygubernator/__init__.py +180 -0
- pygubernator-0.0.1/src/pygubernator/actions/__init__.py +14 -0
- pygubernator-0.0.1/src/pygubernator/actions/executor.py +241 -0
- pygubernator-0.0.1/src/pygubernator/actions/registry.py +287 -0
- pygubernator-0.0.1/src/pygubernator/config/__init__.py +13 -0
- pygubernator-0.0.1/src/pygubernator/config/loader.py +358 -0
- pygubernator-0.0.1/src/pygubernator/config/schemas/machine.json +192 -0
- pygubernator-0.0.1/src/pygubernator/config/validator.py +216 -0
- pygubernator-0.0.1/src/pygubernator/core/__init__.py +36 -0
- pygubernator-0.0.1/src/pygubernator/core/errors.py +271 -0
- pygubernator-0.0.1/src/pygubernator/core/event.py +116 -0
- pygubernator-0.0.1/src/pygubernator/core/machine.py +573 -0
- pygubernator-0.0.1/src/pygubernator/core/state.py +123 -0
- pygubernator-0.0.1/src/pygubernator/core/transition.py +188 -0
- pygubernator-0.0.1/src/pygubernator/guards/__init__.py +15 -0
- pygubernator-0.0.1/src/pygubernator/guards/builtins.py +341 -0
- pygubernator-0.0.1/src/pygubernator/guards/evaluator.py +162 -0
- pygubernator-0.0.1/src/pygubernator/guards/registry.py +262 -0
- pygubernator-0.0.1/src/pygubernator/py.typed +1 -0
- pygubernator-0.0.1/src/pygubernator/timeout/__init__.py +17 -0
- pygubernator-0.0.1/src/pygubernator/timeout/manager.py +348 -0
- pygubernator-0.0.1/src/pygubernator/utils/__init__.py +17 -0
- pygubernator-0.0.1/src/pygubernator/utils/serialization.py +165 -0
- pygubernator-0.0.1/src/pygubernator.egg-info/PKG-INFO +394 -0
- pygubernator-0.0.1/src/pygubernator.egg-info/SOURCES.txt +32 -0
- pygubernator-0.0.1/src/pygubernator.egg-info/dependency_links.txt +1 -0
- pygubernator-0.0.1/src/pygubernator.egg-info/requires.txt +11 -0
- pygubernator-0.0.1/src/pygubernator.egg-info/top_level.txt +1 -0
- pygubernator-0.0.1/tests/test_version.py +10 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
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 IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pygubernator
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A configuration-driven, stateless finite state machine library for Python
|
|
5
|
+
Author-email: StatFYI <contact@statfyi.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/statfyi/pygubernator
|
|
8
|
+
Project-URL: Documentation, https://github.com/statfyi/pygubernator#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/statfyi/pygubernator
|
|
10
|
+
Project-URL: Issues, https://github.com/statfyi/pygubernator/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/statfyi/pygubernator/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: state-machine,fsm,finite-state-machine,workflow,configuration-driven,declarative,guards,transitions,order-management,event-driven
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: pyyaml>=6.0
|
|
29
|
+
Requires-Dist: jsonschema>=4.20.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
35
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: types-PyYAML>=6.0.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# PyGubernator
|
|
41
|
+
|
|
42
|
+
A configuration-driven, stateless finite state machine library for Python.
|
|
43
|
+
|
|
44
|
+
PyGubernator defines behavioral contracts through YAML/JSON specifications, computing state transitions without holding internal state. Designed for high-integrity systems like order management, workflow engines, and distributed applications.
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **Configuration-Driven**: Define state machines in YAML/JSON with schema validation
|
|
49
|
+
- **Stateless Design**: Pure computation—takes state in, returns state/actions out
|
|
50
|
+
- **Guards**: Conditional transitions based on runtime context
|
|
51
|
+
- **Actions/Hooks**: Entry/exit hooks and transition actions
|
|
52
|
+
- **Timeouts/TTL**: Automatic transitions after configurable durations
|
|
53
|
+
- **Strict Mode**: Contract enforcement for undefined triggers
|
|
54
|
+
- **Type-Safe**: Full type hints and PEP 561 compliance
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install pygubernator
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For development:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/statfyi/pygubernator
|
|
66
|
+
cd pygubernator
|
|
67
|
+
pip install -e ".[dev]"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
### 1. Define Your State Machine (YAML)
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
# order_fsm.yaml
|
|
76
|
+
meta:
|
|
77
|
+
version: "1.0.0"
|
|
78
|
+
machine_name: "order_management"
|
|
79
|
+
strict_mode: true
|
|
80
|
+
|
|
81
|
+
states:
|
|
82
|
+
- name: PENDING_NEW
|
|
83
|
+
type: initial
|
|
84
|
+
timeout:
|
|
85
|
+
seconds: 5.0
|
|
86
|
+
destination: TIMED_OUT
|
|
87
|
+
|
|
88
|
+
- name: OPEN
|
|
89
|
+
type: stable
|
|
90
|
+
on_enter:
|
|
91
|
+
- notify_ui
|
|
92
|
+
- log_audit
|
|
93
|
+
|
|
94
|
+
- name: FILLED
|
|
95
|
+
type: terminal
|
|
96
|
+
|
|
97
|
+
- name: TIMED_OUT
|
|
98
|
+
type: terminal
|
|
99
|
+
|
|
100
|
+
transitions:
|
|
101
|
+
- trigger: exchange_ack
|
|
102
|
+
source: PENDING_NEW
|
|
103
|
+
dest: OPEN
|
|
104
|
+
actions:
|
|
105
|
+
- update_order_id
|
|
106
|
+
|
|
107
|
+
- trigger: execution_report
|
|
108
|
+
source: OPEN
|
|
109
|
+
dest: FILLED
|
|
110
|
+
guards:
|
|
111
|
+
- is_full_fill
|
|
112
|
+
actions:
|
|
113
|
+
- update_positions
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 2. Use the State Machine
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from pygubernator import StateMachine, GuardRegistry, ActionRegistry, Event
|
|
120
|
+
from pygubernator.actions import ActionExecutor
|
|
121
|
+
|
|
122
|
+
# Load the FSM definition
|
|
123
|
+
machine = StateMachine.from_yaml("order_fsm.yaml")
|
|
124
|
+
|
|
125
|
+
# Register guards (pure functions for conditions)
|
|
126
|
+
guards = GuardRegistry()
|
|
127
|
+
guards.register("is_full_fill", lambda ctx: ctx["fill_qty"] >= ctx["order_qty"])
|
|
128
|
+
machine.bind_guards(guards)
|
|
129
|
+
|
|
130
|
+
# Register actions (side effects executed after persistence)
|
|
131
|
+
actions = ActionRegistry()
|
|
132
|
+
actions.register("update_order_id", lambda ctx: update_db(ctx["order_id"]))
|
|
133
|
+
actions.register("update_positions", lambda ctx: update_positions(ctx))
|
|
134
|
+
actions.register("notify_ui", lambda ctx: send_notification(ctx))
|
|
135
|
+
actions.register("log_audit", lambda ctx: log_audit_trail(ctx))
|
|
136
|
+
|
|
137
|
+
executor = ActionExecutor(actions)
|
|
138
|
+
|
|
139
|
+
# --- The "Sandwich Pattern" ---
|
|
140
|
+
|
|
141
|
+
# Phase 1: Receive event
|
|
142
|
+
event = Event(trigger="execution_report", payload={"fill_qty": 100, "order_qty": 100})
|
|
143
|
+
|
|
144
|
+
# Phase 2: Get current state from your database
|
|
145
|
+
current_state = db.get_order_state(order_id) # "OPEN"
|
|
146
|
+
|
|
147
|
+
# Phase 3: Compute transition (pure, no side effects)
|
|
148
|
+
result = machine.process(
|
|
149
|
+
current_state=current_state,
|
|
150
|
+
event=event,
|
|
151
|
+
context={"fill_qty": 100, "order_qty": 100}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Phase 4: Persist atomically
|
|
155
|
+
if result.success:
|
|
156
|
+
with db.transaction():
|
|
157
|
+
db.update_order_state(order_id, result.target_state)
|
|
158
|
+
db.insert_audit_trail(order_id, result)
|
|
159
|
+
|
|
160
|
+
# Phase 5: Execute side effects (after commit)
|
|
161
|
+
executor.execute(result, context)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Core Concepts
|
|
165
|
+
|
|
166
|
+
### States
|
|
167
|
+
|
|
168
|
+
States represent the nodes in your state machine:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from pygubernator import State, StateType, Timeout
|
|
172
|
+
|
|
173
|
+
state = State(
|
|
174
|
+
name="PENDING_NEW",
|
|
175
|
+
type=StateType.INITIAL, # initial, stable, terminal, error
|
|
176
|
+
description="Waiting for exchange acknowledgment",
|
|
177
|
+
on_enter=("log_entry",),
|
|
178
|
+
on_exit=("log_exit",),
|
|
179
|
+
timeout=Timeout(seconds=5.0, destination="TIMED_OUT"),
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Transitions
|
|
184
|
+
|
|
185
|
+
Transitions define the valid paths between states:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from pygubernator import Transition
|
|
189
|
+
|
|
190
|
+
transition = Transition(
|
|
191
|
+
trigger="execution_report",
|
|
192
|
+
source=frozenset({"OPEN", "PARTIALLY_FILLED"}),
|
|
193
|
+
dest="FILLED",
|
|
194
|
+
guards=("is_full_fill",),
|
|
195
|
+
actions=("update_positions", "release_buying_power"),
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Guards
|
|
200
|
+
|
|
201
|
+
Guards are pure functions that control whether transitions are allowed:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from pygubernator import GuardRegistry, equals, greater_than, all_of
|
|
205
|
+
|
|
206
|
+
guards = GuardRegistry()
|
|
207
|
+
|
|
208
|
+
# Simple function
|
|
209
|
+
guards.register("is_full_fill", lambda ctx: ctx["fill_qty"] >= ctx["order_qty"])
|
|
210
|
+
|
|
211
|
+
# Built-in guard factories
|
|
212
|
+
guards.register("is_valid_amount", greater_than("amount", 0))
|
|
213
|
+
guards.register("is_admin", equals("role", "admin"))
|
|
214
|
+
|
|
215
|
+
# Compound guards
|
|
216
|
+
guards.register(
|
|
217
|
+
"can_approve",
|
|
218
|
+
all_of(equals("status", "pending"), greater_than("balance", 1000))
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Actions
|
|
223
|
+
|
|
224
|
+
Actions handle side effects and are executed after state persistence:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from pygubernator import ActionRegistry
|
|
228
|
+
from pygubernator.actions import ActionExecutor
|
|
229
|
+
|
|
230
|
+
actions = ActionRegistry()
|
|
231
|
+
|
|
232
|
+
@actions.decorator()
|
|
233
|
+
def send_notification(ctx: dict) -> None:
|
|
234
|
+
email_service.send(ctx["user_email"], "Order updated")
|
|
235
|
+
|
|
236
|
+
@actions.decorator(name="update_ledger")
|
|
237
|
+
def update_ledger_entry(ctx: dict) -> None:
|
|
238
|
+
ledger.record_transaction(ctx["order_id"], ctx["amount"])
|
|
239
|
+
|
|
240
|
+
# Execute after successful DB commit
|
|
241
|
+
executor = ActionExecutor(actions)
|
|
242
|
+
execution_result = executor.execute(transition_result, context)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Timeouts
|
|
246
|
+
|
|
247
|
+
Handle automatic transitions when entities stay in a state too long:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from pygubernator import TimeoutManager, check_timeout
|
|
251
|
+
from datetime import datetime, timezone
|
|
252
|
+
|
|
253
|
+
manager = TimeoutManager(machine)
|
|
254
|
+
|
|
255
|
+
# Check if order has timed out
|
|
256
|
+
entered_at = datetime.fromisoformat(order["entered_pending_at"])
|
|
257
|
+
timeout_result = check_timeout(machine, "PENDING_NEW", entered_at)
|
|
258
|
+
|
|
259
|
+
if timeout_result:
|
|
260
|
+
# Process the timeout transition
|
|
261
|
+
db.update_order_state(order_id, timeout_result.target_state)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## The Sandwich Pattern
|
|
265
|
+
|
|
266
|
+
PyGubernator is designed around the "Load → Decide → Commit → Act" pattern for high-integrity systems:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
270
|
+
│ 1. INGRESS: Receive event, normalize to trigger + payload │
|
|
271
|
+
├─────────────────────────────────────────────────────────────┤
|
|
272
|
+
│ 2. HYDRATION: Load current state from database │
|
|
273
|
+
├─────────────────────────────────────────────────────────────┤
|
|
274
|
+
│ 3. COMPUTE: machine.process() - pure, no side effects │
|
|
275
|
+
├─────────────────────────────────────────────────────────────┤
|
|
276
|
+
│ 4. PERSIST: Atomic DB transaction (state + audit trail) │
|
|
277
|
+
├─────────────────────────────────────────────────────────────┤
|
|
278
|
+
│ 5. EXECUTE: Run actions AFTER successful commit │
|
|
279
|
+
└─────────────────────────────────────────────────────────────┘
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
This pattern ensures:
|
|
283
|
+
- **Atomicity**: State changes are persisted atomically
|
|
284
|
+
- **Idempotency**: Same input always produces same output
|
|
285
|
+
- **Recoverability**: Actions can be retried independently
|
|
286
|
+
- **Horizontal scaling**: No shared state in the library
|
|
287
|
+
|
|
288
|
+
## Configuration Schema
|
|
289
|
+
|
|
290
|
+
PyGubernator validates your YAML/JSON configuration against a JSON Schema:
|
|
291
|
+
|
|
292
|
+
```yaml
|
|
293
|
+
meta:
|
|
294
|
+
version: "1.0.0" # Semantic version
|
|
295
|
+
machine_name: "my_fsm" # Unique identifier
|
|
296
|
+
strict_mode: true # Raise on undefined triggers
|
|
297
|
+
|
|
298
|
+
states:
|
|
299
|
+
- name: STATE_NAME # UPPER_SNAKE_CASE
|
|
300
|
+
type: initial|stable|terminal|error
|
|
301
|
+
description: "Human readable"
|
|
302
|
+
on_enter: [action1, action2]
|
|
303
|
+
on_exit: [action3]
|
|
304
|
+
timeout:
|
|
305
|
+
seconds: 5.0
|
|
306
|
+
destination: TIMEOUT_STATE
|
|
307
|
+
|
|
308
|
+
transitions:
|
|
309
|
+
- trigger: event_name # lower_snake_case
|
|
310
|
+
source: STATE_A # or [STATE_A, STATE_B]
|
|
311
|
+
dest: STATE_B
|
|
312
|
+
guards: [guard1, guard2]
|
|
313
|
+
actions: [action1]
|
|
314
|
+
|
|
315
|
+
error_policy:
|
|
316
|
+
default_fallback: ERROR_STATE
|
|
317
|
+
retry_attempts: 3
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## API Reference
|
|
321
|
+
|
|
322
|
+
### StateMachine
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
# Creation
|
|
326
|
+
machine = StateMachine.from_yaml("path/to/config.yaml")
|
|
327
|
+
machine = StateMachine.from_dict(config_dict)
|
|
328
|
+
|
|
329
|
+
# Processing
|
|
330
|
+
result = machine.process(current_state, trigger_or_event, context)
|
|
331
|
+
|
|
332
|
+
# Queries
|
|
333
|
+
machine.get_state("STATE_NAME")
|
|
334
|
+
machine.get_initial_state()
|
|
335
|
+
machine.get_available_transitions("STATE_NAME")
|
|
336
|
+
machine.get_available_triggers("STATE_NAME")
|
|
337
|
+
machine.validate_state("STATE_NAME")
|
|
338
|
+
machine.is_terminal("STATE_NAME")
|
|
339
|
+
machine.can_transition("STATE_A", "trigger", context)
|
|
340
|
+
|
|
341
|
+
# Properties
|
|
342
|
+
machine.name
|
|
343
|
+
machine.version
|
|
344
|
+
machine.states
|
|
345
|
+
machine.transitions
|
|
346
|
+
machine.state_names
|
|
347
|
+
machine.trigger_names
|
|
348
|
+
machine.terminal_states
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### TransitionResult
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
result = machine.process(state, trigger, context)
|
|
355
|
+
|
|
356
|
+
result.success # bool
|
|
357
|
+
result.source_state # str
|
|
358
|
+
result.target_state # str | None
|
|
359
|
+
result.trigger # str
|
|
360
|
+
result.actions_to_execute # tuple[str, ...]
|
|
361
|
+
result.on_exit_actions # tuple[str, ...]
|
|
362
|
+
result.on_enter_actions # tuple[str, ...]
|
|
363
|
+
result.all_actions # tuple[str, ...] (exit + transition + enter)
|
|
364
|
+
result.error # FSMError | None
|
|
365
|
+
result.state_changed # bool
|
|
366
|
+
result.is_self_transition # bool
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Development
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Install dev dependencies
|
|
373
|
+
pip install -e ".[dev]"
|
|
374
|
+
|
|
375
|
+
# Run tests
|
|
376
|
+
pytest
|
|
377
|
+
|
|
378
|
+
# Run tests with coverage
|
|
379
|
+
pytest --cov=pygubernator --cov-report=term-missing
|
|
380
|
+
|
|
381
|
+
# Type checking
|
|
382
|
+
mypy src/
|
|
383
|
+
|
|
384
|
+
# Linting & formatting
|
|
385
|
+
ruff check .
|
|
386
|
+
ruff format .
|
|
387
|
+
|
|
388
|
+
# Run all checks
|
|
389
|
+
make check
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## License
|
|
393
|
+
|
|
394
|
+
MIT
|