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.
Files changed (34) hide show
  1. pygubernator-0.0.1/LICENSE +21 -0
  2. pygubernator-0.0.1/PKG-INFO +394 -0
  3. pygubernator-0.0.1/README.md +355 -0
  4. pygubernator-0.0.1/pyproject.toml +130 -0
  5. pygubernator-0.0.1/setup.cfg +4 -0
  6. pygubernator-0.0.1/src/pygubernator/__init__.py +180 -0
  7. pygubernator-0.0.1/src/pygubernator/actions/__init__.py +14 -0
  8. pygubernator-0.0.1/src/pygubernator/actions/executor.py +241 -0
  9. pygubernator-0.0.1/src/pygubernator/actions/registry.py +287 -0
  10. pygubernator-0.0.1/src/pygubernator/config/__init__.py +13 -0
  11. pygubernator-0.0.1/src/pygubernator/config/loader.py +358 -0
  12. pygubernator-0.0.1/src/pygubernator/config/schemas/machine.json +192 -0
  13. pygubernator-0.0.1/src/pygubernator/config/validator.py +216 -0
  14. pygubernator-0.0.1/src/pygubernator/core/__init__.py +36 -0
  15. pygubernator-0.0.1/src/pygubernator/core/errors.py +271 -0
  16. pygubernator-0.0.1/src/pygubernator/core/event.py +116 -0
  17. pygubernator-0.0.1/src/pygubernator/core/machine.py +573 -0
  18. pygubernator-0.0.1/src/pygubernator/core/state.py +123 -0
  19. pygubernator-0.0.1/src/pygubernator/core/transition.py +188 -0
  20. pygubernator-0.0.1/src/pygubernator/guards/__init__.py +15 -0
  21. pygubernator-0.0.1/src/pygubernator/guards/builtins.py +341 -0
  22. pygubernator-0.0.1/src/pygubernator/guards/evaluator.py +162 -0
  23. pygubernator-0.0.1/src/pygubernator/guards/registry.py +262 -0
  24. pygubernator-0.0.1/src/pygubernator/py.typed +1 -0
  25. pygubernator-0.0.1/src/pygubernator/timeout/__init__.py +17 -0
  26. pygubernator-0.0.1/src/pygubernator/timeout/manager.py +348 -0
  27. pygubernator-0.0.1/src/pygubernator/utils/__init__.py +17 -0
  28. pygubernator-0.0.1/src/pygubernator/utils/serialization.py +165 -0
  29. pygubernator-0.0.1/src/pygubernator.egg-info/PKG-INFO +394 -0
  30. pygubernator-0.0.1/src/pygubernator.egg-info/SOURCES.txt +32 -0
  31. pygubernator-0.0.1/src/pygubernator.egg-info/dependency_links.txt +1 -0
  32. pygubernator-0.0.1/src/pygubernator.egg-info/requires.txt +11 -0
  33. pygubernator-0.0.1/src/pygubernator.egg-info/top_level.txt +1 -0
  34. 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