flask-stateflow 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.
- flask_stateflow-0.1.0/PKG-INFO +7 -0
- flask_stateflow-0.1.0/setup.cfg +4 -0
- flask_stateflow-0.1.0/setup.py +10 -0
- flask_stateflow-0.1.0/src/flask_stateflow/__init__.py +11 -0
- flask_stateflow-0.1.0/src/flask_stateflow/stateflow.py +169 -0
- flask_stateflow-0.1.0/src/flask_stateflow.egg-info/PKG-INFO +7 -0
- flask_stateflow-0.1.0/src/flask_stateflow.egg-info/SOURCES.txt +7 -0
- flask_stateflow-0.1.0/src/flask_stateflow.egg-info/dependency_links.txt +1 -0
- flask_stateflow-0.1.0/src/flask_stateflow.egg-info/top_level.txt +1 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flask_stateflow
|