flask-stateflow 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,11 @@
1
+ from src.flask_stateflow.stateflow import (
2
+ StateMachine,
3
+ transition,
4
+ guard,
5
+ on_enter,
6
+ on_exit,
7
+ validate,
8
+ InvalidTransitionError,
9
+ StateNotFoundError,
10
+ GuardBlockedError,
11
+ )
@@ -0,0 +1,169 @@
1
+ #A decorator that marks a method as a state transition handler.
2
+ def transition(from_state, to_state, guards=None):
3
+ def decorator(fn):
4
+ fn._sf = {'from': from_state, 'to': to_state, 'guards': guards or []}
5
+ return fn
6
+ return decorator
7
+
8
+ #A decorator that marks a method as guard, which is a check before a transition.
9
+ def guard(name=None):
10
+ def decorator(fn):
11
+ fn._sf_guard = name or fn.__name__
12
+ return fn
13
+ return decorator
14
+
15
+ #A decorator that marks a method as a hook that is called when the specified state is entered
16
+ def on_enter(state):
17
+ def decorator(fn):
18
+ fn._sf_enter = state
19
+ return fn
20
+ return decorator
21
+
22
+ #A decorator that marks a method as a hook that is called when the specified state is exited.
23
+ def on_exit(state):
24
+ def decorator(fn):
25
+ fn._sf_exit = state
26
+ return fn
27
+ return decorator
28
+
29
+ def validate(cls):
30
+ errs = []
31
+ states = set(cls.__states__)
32
+ for fr, tos in cls.__transitions__.items():
33
+ if fr not in states:
34
+ errs.append(f"Unknown from-state: '{fr}'")
35
+ for to in tos:
36
+ if to not in states:
37
+ errs.append(f"Unknown to-state: '{to}'")
38
+ for fr, tos in cls._sf_trans.items():
39
+ for to, cfg in tos.items():
40
+ for g in cfg.get('guards', []):
41
+ if g not in cls._sf_guards:
42
+ errs.append(f"Missing guard '{g}' for {fr}->{to}")
43
+ return errs
44
+
45
+ #A metaclass is a class that creates classes.
46
+ class _Meta(type):
47
+ def __new__(mcs, name, bases, ns):
48
+ cls = super().__new__(mcs, name, bases, ns)
49
+ trans, guards, enters, exits = {}, {}, {}, {}
50
+
51
+ for base in bases:
52
+ trans.update(getattr(base, '_sf_trans', {}))
53
+ guards.update(getattr(base, '_sf_guards', {}))
54
+ for st, fns in getattr(base, '_sf_enters', {}).items():
55
+ enters.setdefault(st, []).extend(fns)
56
+ for st, fns in getattr(base, '_sf_exits', {}).items():
57
+ exits.setdefault(st, []).extend(fns)
58
+
59
+ for attr in ns.values():
60
+ if not callable(attr):
61
+ continue
62
+ if hasattr(attr, '_sf'):
63
+ c = attr._sf
64
+ trans.setdefault(c['from'], {})[c['to']] = {
65
+ 'fn': attr.__name__, 'guards': c['guards']
66
+ }
67
+ if hasattr(attr, '_sf_guard'):
68
+ guards[attr._sf_guard] = attr
69
+ if hasattr(attr, '_sf_enter'):
70
+ enters.setdefault(attr._sf_enter, []).append(attr)
71
+ if hasattr(attr, '_sf_exit'):
72
+ exits.setdefault(attr._sf_exit, []).append(attr)
73
+
74
+ cls._sf_trans = trans
75
+ cls._sf_guards = guards
76
+ cls._sf_enters = enters
77
+ cls._sf_exits = exits
78
+ return cls
79
+
80
+ class InvalidTransitionError(Exception):
81
+ def __init__(self, current, target, allowed=None):
82
+ msg = f"'{current}' -> '{target}' not allowed"
83
+ if allowed:
84
+ msg += f", allowed from '{current}': {allowed}"
85
+ super().__init__(msg)
86
+
87
+ class GuardBlockedError(Exception):
88
+ def __init__(self, name, reason=None):
89
+ msg = f"Guard '{name}' blocked"
90
+ if reason:
91
+ msg += f": {reason}"
92
+ super().__init__(msg)
93
+
94
+
95
+ class StateNotFoundError(Exception):
96
+ def __init__(self, state, available=None):
97
+ msg = f"State '{state}' not found"
98
+ if available:
99
+ msg += f", available: {available}"
100
+ super().__init__(msg)
101
+
102
+ class StateMachine(metaclass=_Meta):
103
+ __states__ = []
104
+ __transitions__ = {}
105
+ __state_column__ = 'state'
106
+ __state_default__ = None
107
+
108
+ def __init__(self, *args, **kwargs):
109
+ super().__init__(*args, **kwargs)
110
+ self._sf_state = self.__state_default__ or (self.__states__[0] if self.__states__ else None)
111
+
112
+ @property
113
+ def current_state(self):
114
+ col = self.__state_column__
115
+ if hasattr(self, col) and not col.startswith('_'):
116
+ v = getattr(self, col)
117
+ if v is not None:
118
+ return v
119
+ return self._sf_state
120
+
121
+ @current_state.setter
122
+ def current_state(self, v):
123
+ col = self.__state_column__
124
+ if hasattr(self, col) and not col.startswith('_'):
125
+ setattr(self, col, v)
126
+ self._sf_state = v
127
+
128
+ @classmethod
129
+ def can_transition(cls, fr, to):
130
+ return to in cls.__transitions__.get(fr, [])
131
+
132
+ @classmethod
133
+ def get_allowed(cls, fr):
134
+ return list(cls.__transitions__.get(fr, []))
135
+
136
+ def transition_to(self, new_state, **kwargs):
137
+ cur = self.current_state
138
+
139
+ if new_state not in self.__states__:
140
+ raise StateNotFoundError(new_state, self.__states__)
141
+ if not self.can_transition(cur, new_state):
142
+ raise InvalidTransitionError(cur, new_state, self.get_allowed(cur))
143
+
144
+ cfg = self._sf_trans.get(cur, {}).get(new_state, {})
145
+
146
+ for gname in cfg.get('guards', []):
147
+ g = self._sf_guards.get(gname)
148
+ if not g:
149
+ raise GuardBlockedError(gname, "not found")
150
+ r = g(self)
151
+ if r is not True:
152
+ raise GuardBlockedError(gname, r if isinstance(r, str) else None)
153
+
154
+ for h in self._sf_exits.get(cur, []):
155
+ h(self)
156
+
157
+ fn = cfg.get('fn')
158
+ if fn:
159
+ getattr(self, fn)(**kwargs)
160
+
161
+ self.current_state = new_state
162
+
163
+ for h in self._sf_enters.get(new_state, []):
164
+ h(self)
165
+
166
+ return True
167
+
168
+ def __repr__(self):
169
+ return f"<{type(self).__name__} {self.current_state}>"
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: flask-stateflow
3
+ Version: 0.1.0
4
+ Summary: Minimal declarative state machine
5
+ Requires-Python: >=3.8
6
+ Dynamic: requires-python
7
+ Dynamic: summary
@@ -0,0 +1,6 @@
1
+ flask_stateflow/__init__.py,sha256=c_o3CeHJGNMuQ2fh6R8ypwGHiINLJ7rqQVNDR9GAcb8,216
2
+ flask_stateflow/stateflow.py,sha256=KiQxJwYOBZpYw8nGpTefbTxSoMhfkkyZYVVK-QXAf_o,5728
3
+ flask_stateflow-0.1.0.dist-info/METADATA,sha256=YHtR3AXWEJovn3-YF7Zk-JVmDa-X_Yrubo_HKnoadKg,174
4
+ flask_stateflow-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ flask_stateflow-0.1.0.dist-info/top_level.txt,sha256=EeMKGhxUP3Uropg2_jsD9dSgRdBwu_LkhVka0J37SsA,16
6
+ flask_stateflow-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ flask_stateflow