smallib 1.0.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.
- small/__init__.py +39 -0
- small/fsm/__init__.py +50 -0
- small/fsm/auto.py +74 -0
- small/fsm/base.py +290 -0
- small/fsm/machine.py +226 -0
- small/lock/__init__.py +44 -0
- small/lock/lock.py +73 -0
- small/log/__init__.py +47 -0
- small/log/log.py +127 -0
- small/skywalker/__init__.py +45 -0
- small/skywalker/daemon.py +101 -0
- small/skywalker/runner.py +178 -0
- small/skywalker/ticker.py +179 -0
- small/utils/__init__.py +64 -0
- smallib-1.0.0.dist-info/METADATA +52 -0
- smallib-1.0.0.dist-info/RECORD +18 -0
- smallib-1.0.0.dist-info/WHEEL +5 -0
- smallib-1.0.0.dist-info/top_level.txt +1 -0
small/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# SMALL: State Machine, Async Lock, Logger
|
|
4
|
+
#
|
|
5
|
+
# Written in 2026 by Moky <albert.moky@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
# MIT License
|
|
9
|
+
#
|
|
10
|
+
# Copyright (c) 2026 Albert Moky
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
# furnished to do so, subject to the following conditions:
|
|
18
|
+
#
|
|
19
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
# copies or substantial portions of the Software.
|
|
21
|
+
#
|
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
# SOFTWARE.
|
|
29
|
+
# ==============================================================================
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
name = "SMALL"
|
|
33
|
+
|
|
34
|
+
__author__ = 'Albert Moky'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
|
|
39
|
+
]
|
small/fsm/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Finite State Machine
|
|
4
|
+
#
|
|
5
|
+
# Written in 2021 by Moky <albert.moky@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
# MIT License
|
|
9
|
+
#
|
|
10
|
+
# Copyright (c) 2021 Albert Moky
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
# furnished to do so, subject to the following conditions:
|
|
18
|
+
#
|
|
19
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
# copies or substantial portions of the Software.
|
|
21
|
+
#
|
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
# SOFTWARE.
|
|
29
|
+
# ==============================================================================
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
Finite State Machine
|
|
33
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from .machine import Context, Transition, State, Machine, Delegate
|
|
37
|
+
from .base import BaseTransition, BaseState, BaseMachine
|
|
38
|
+
from .auto import AutoMachine
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
name = "FSM"
|
|
42
|
+
|
|
43
|
+
__author__ = 'Albert Moky'
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
|
|
47
|
+
'Context', 'Transition', 'State', 'Machine', 'Delegate',
|
|
48
|
+
'BaseTransition', 'BaseState', 'BaseMachine',
|
|
49
|
+
'AutoMachine',
|
|
50
|
+
]
|
small/fsm/auto.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Finite State Machine
|
|
4
|
+
#
|
|
5
|
+
# Written in 2021 by Moky <albert.moky@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
# MIT License
|
|
9
|
+
#
|
|
10
|
+
# Copyright (c) 2021 Albert Moky
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
# furnished to do so, subject to the following conditions:
|
|
18
|
+
#
|
|
19
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
# copies or substantial portions of the Software.
|
|
21
|
+
#
|
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
# SOFTWARE.
|
|
29
|
+
# ==============================================================================
|
|
30
|
+
|
|
31
|
+
from abc import ABC # , abstractmethod
|
|
32
|
+
|
|
33
|
+
from ..skywalker import PrimeMetronome
|
|
34
|
+
|
|
35
|
+
from .machine import S, C, U, T
|
|
36
|
+
from .base import BaseMachine
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# noinspection PyAbstractClass
|
|
40
|
+
class AutoMachine(BaseMachine[C, T, S], ABC):
|
|
41
|
+
|
|
42
|
+
# @property # Override
|
|
43
|
+
# @abstractmethod
|
|
44
|
+
# def context(self) -> C:
|
|
45
|
+
# """ machine itself """
|
|
46
|
+
# raise NotImplementedError(
|
|
47
|
+
# f'Not implemented: {type(self).__module__}.{type(self).__name__}.context getter'
|
|
48
|
+
# )
|
|
49
|
+
|
|
50
|
+
# Override
|
|
51
|
+
async def start(self) -> bool:
|
|
52
|
+
ok = await super().start()
|
|
53
|
+
timer = PrimeMetronome()
|
|
54
|
+
timer.add_ticker(ticker=self)
|
|
55
|
+
return ok
|
|
56
|
+
|
|
57
|
+
# Override
|
|
58
|
+
async def stop(self) -> bool:
|
|
59
|
+
timer = PrimeMetronome()
|
|
60
|
+
timer.remove_ticker(ticker=self)
|
|
61
|
+
return await super().stop()
|
|
62
|
+
|
|
63
|
+
# Override
|
|
64
|
+
async def pause(self) -> bool:
|
|
65
|
+
timer = PrimeMetronome()
|
|
66
|
+
timer.remove_ticker(ticker=self)
|
|
67
|
+
return await super().pause()
|
|
68
|
+
|
|
69
|
+
# Override
|
|
70
|
+
async def resume(self) -> bool:
|
|
71
|
+
ok = await super().resume()
|
|
72
|
+
timer = PrimeMetronome()
|
|
73
|
+
timer.add_ticker(ticker=self)
|
|
74
|
+
return ok
|
small/fsm/base.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Finite State Machine
|
|
4
|
+
#
|
|
5
|
+
# Written in 2021 by Moky <albert.moky@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
# MIT License
|
|
9
|
+
#
|
|
10
|
+
# Copyright (c) 2021 Albert Moky
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
# furnished to do so, subject to the following conditions:
|
|
18
|
+
#
|
|
19
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
# copies or substantial portions of the Software.
|
|
21
|
+
#
|
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
# SOFTWARE.
|
|
29
|
+
# ==============================================================================
|
|
30
|
+
|
|
31
|
+
import time
|
|
32
|
+
import weakref
|
|
33
|
+
from abc import ABC, abstractmethod
|
|
34
|
+
from enum import IntEnum
|
|
35
|
+
from typing import List, Optional
|
|
36
|
+
|
|
37
|
+
from ..utils import Timestamp, Duration
|
|
38
|
+
|
|
39
|
+
from .machine import S, C, U, T
|
|
40
|
+
from .machine import Transition, State, Machine, Delegate
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# noinspection PyAbstractClass
|
|
44
|
+
class BaseTransition(Transition[C], ABC):
|
|
45
|
+
""" Transition with the index of target state """
|
|
46
|
+
|
|
47
|
+
def __init__(self, target: int):
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.__target = target
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def target(self) -> int:
|
|
53
|
+
""" target state index """
|
|
54
|
+
return self.__target
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# noinspection PyAbstractClass
|
|
58
|
+
class BaseState(State[C, T], ABC):
|
|
59
|
+
""" State with transitions """
|
|
60
|
+
|
|
61
|
+
def __init__(self, index: int):
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.__index = index
|
|
64
|
+
self.__transitions: List[Transition[C]] = []
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def index(self) -> int:
|
|
68
|
+
return self.__index
|
|
69
|
+
|
|
70
|
+
def add_transition(self, transition: Transition[C]):
|
|
71
|
+
assert transition not in self.__transitions, 'transition exists: %s' % transition
|
|
72
|
+
self.__transitions.append(transition)
|
|
73
|
+
|
|
74
|
+
# Override
|
|
75
|
+
def evaluate(self, ctx: C, now: Timestamp) -> Optional[T]:
|
|
76
|
+
for trans in self.__transitions:
|
|
77
|
+
if trans.evaluate(ctx, now=now):
|
|
78
|
+
# OK, get target state from this transition
|
|
79
|
+
return trans
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MachineStatus(IntEnum):
|
|
83
|
+
""" Machine Status """
|
|
84
|
+
STOPPED = 0
|
|
85
|
+
RUNNING = 1
|
|
86
|
+
PAUSED = 2
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BaseMachine(Machine[C, T, S], ABC):
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
super().__init__()
|
|
93
|
+
self.__states: List[S] = []
|
|
94
|
+
self.__current = -1 # current state index
|
|
95
|
+
self.__status = MachineStatus.STOPPED
|
|
96
|
+
self.__delegate_ref: Optional[weakref.ReferenceType] = None
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def delegate(self) -> Optional[Delegate[C, T, S]]:
|
|
100
|
+
ref = self.__delegate_ref
|
|
101
|
+
if ref is not None:
|
|
102
|
+
return ref()
|
|
103
|
+
|
|
104
|
+
@delegate.setter
|
|
105
|
+
def delegate(self, handler: Delegate[C, T, S]):
|
|
106
|
+
self.__delegate_ref = None if handler is None else weakref.ref(handler)
|
|
107
|
+
|
|
108
|
+
@property # protected
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def context(self) -> C:
|
|
111
|
+
""" machine itself """
|
|
112
|
+
raise NotImplementedError(
|
|
113
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.context getter'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
#
|
|
117
|
+
# States
|
|
118
|
+
#
|
|
119
|
+
def add_state(self, state: BaseState[C, T]) -> Optional[S]:
|
|
120
|
+
index = state.index
|
|
121
|
+
assert index >= 0, 'state index error: %d' % index
|
|
122
|
+
count = len(self.__states)
|
|
123
|
+
if index < count:
|
|
124
|
+
# WARNING: return old state that was replaced
|
|
125
|
+
old = self.__states[index]
|
|
126
|
+
self.__states[index] = state
|
|
127
|
+
return old
|
|
128
|
+
# filling empty spaces
|
|
129
|
+
spaces = index - count
|
|
130
|
+
for i in range(spaces):
|
|
131
|
+
self.__states.append(None)
|
|
132
|
+
# append the new state to the tail
|
|
133
|
+
self.__states.append(state)
|
|
134
|
+
|
|
135
|
+
def get_state(self, index: int) -> Optional[State[C, T]]:
|
|
136
|
+
return self.__states[index]
|
|
137
|
+
|
|
138
|
+
@property # protected
|
|
139
|
+
def default_state(self) -> Optional[State[C, T]]:
|
|
140
|
+
return self.__states[0]
|
|
141
|
+
|
|
142
|
+
# protected
|
|
143
|
+
def get_target_state(self, transition: BaseTransition[C]) -> State[C, T]:
|
|
144
|
+
# Get target state of this transition
|
|
145
|
+
return self.__states[transition.target]
|
|
146
|
+
|
|
147
|
+
@property # Override
|
|
148
|
+
def current_state(self) -> Optional[State[C, T]]:
|
|
149
|
+
index = self.__current
|
|
150
|
+
if 0 <= index: # and index < len(self.__states):
|
|
151
|
+
return self.__states[index]
|
|
152
|
+
|
|
153
|
+
@current_state.setter # private
|
|
154
|
+
def current_state(self, state: BaseState[C, T]):
|
|
155
|
+
self.__current = -1 if state is None else state.index
|
|
156
|
+
|
|
157
|
+
async def __change_state(self, state: Optional[State[C, T]], now: Timestamp) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Exit current state, and enter new state
|
|
160
|
+
|
|
161
|
+
:param state: next state
|
|
162
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
163
|
+
"""
|
|
164
|
+
old = self.current_state
|
|
165
|
+
if old == state:
|
|
166
|
+
# Log.info('[FSM] state not change: %s', state)
|
|
167
|
+
return False
|
|
168
|
+
machine = self.context
|
|
169
|
+
delegate = self.delegate
|
|
170
|
+
#
|
|
171
|
+
# Events before state changed
|
|
172
|
+
#
|
|
173
|
+
if delegate is not None:
|
|
174
|
+
# prepare for changing current state to the new one,
|
|
175
|
+
# the delegate can get old state via ctx if need
|
|
176
|
+
await delegate.enter_state(state, machine, now=now)
|
|
177
|
+
if old is not None:
|
|
178
|
+
await old.on_exit(state, machine, now=now)
|
|
179
|
+
#
|
|
180
|
+
# Change current state
|
|
181
|
+
#
|
|
182
|
+
self.current_state = state
|
|
183
|
+
#
|
|
184
|
+
# Events after state changed
|
|
185
|
+
#
|
|
186
|
+
if state is not None:
|
|
187
|
+
await state.on_enter(old, machine, now=now)
|
|
188
|
+
if delegate is not None:
|
|
189
|
+
# handle after the current state changed,
|
|
190
|
+
# the delegate can get new state via ctx if need
|
|
191
|
+
await delegate.exit_state(old, machine, now=now)
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
#
|
|
195
|
+
# Actions
|
|
196
|
+
#
|
|
197
|
+
|
|
198
|
+
# Override
|
|
199
|
+
async def start(self) -> bool:
|
|
200
|
+
if self.__status != MachineStatus.STOPPED:
|
|
201
|
+
# running or paused
|
|
202
|
+
# cannot start again
|
|
203
|
+
return False
|
|
204
|
+
now = time.time()
|
|
205
|
+
ok = await self.__change_state(state=self.default_state, now=now)
|
|
206
|
+
# assert ok, 'failed to change default state'
|
|
207
|
+
self.__status = MachineStatus.RUNNING
|
|
208
|
+
return ok
|
|
209
|
+
|
|
210
|
+
# Override
|
|
211
|
+
async def stop(self) -> bool:
|
|
212
|
+
if self.__status == MachineStatus.STOPPED:
|
|
213
|
+
# stopped,
|
|
214
|
+
# cannot stop again
|
|
215
|
+
return False
|
|
216
|
+
self.__status = MachineStatus.STOPPED
|
|
217
|
+
now = time.time()
|
|
218
|
+
return await self.__change_state(state=None, now=now) # force current state to None
|
|
219
|
+
|
|
220
|
+
# Override
|
|
221
|
+
async def pause(self) -> bool:
|
|
222
|
+
if self.__status != MachineStatus.RUNNING:
|
|
223
|
+
# paused or stopped,
|
|
224
|
+
# cannot pause now
|
|
225
|
+
return False
|
|
226
|
+
now = time.time()
|
|
227
|
+
machine = self.context
|
|
228
|
+
current = self.current_state
|
|
229
|
+
#
|
|
230
|
+
# Events before state paused
|
|
231
|
+
#
|
|
232
|
+
if current is not None:
|
|
233
|
+
await current.on_pause(machine, now=now)
|
|
234
|
+
#
|
|
235
|
+
# Pause state
|
|
236
|
+
#
|
|
237
|
+
self.__status = MachineStatus.PAUSED
|
|
238
|
+
#
|
|
239
|
+
# Events after state paused
|
|
240
|
+
#
|
|
241
|
+
delegate = self.delegate
|
|
242
|
+
if delegate is not None:
|
|
243
|
+
await delegate.pause_state(current, machine, now=now)
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
# Override
|
|
247
|
+
async def resume(self) -> bool:
|
|
248
|
+
if self.__status != MachineStatus.PAUSED:
|
|
249
|
+
# running or stopped,
|
|
250
|
+
# cannot resume now
|
|
251
|
+
return False
|
|
252
|
+
now = time.time()
|
|
253
|
+
machine = self.context
|
|
254
|
+
current = self.current_state
|
|
255
|
+
#
|
|
256
|
+
# Events before state resumed
|
|
257
|
+
#
|
|
258
|
+
delegate = self.delegate
|
|
259
|
+
if delegate is not None:
|
|
260
|
+
await delegate.resume_state(current, machine, now=now)
|
|
261
|
+
#
|
|
262
|
+
# Resume state
|
|
263
|
+
#
|
|
264
|
+
self.__status = MachineStatus.RUNNING
|
|
265
|
+
#
|
|
266
|
+
# Events after state resumed
|
|
267
|
+
#
|
|
268
|
+
if current is not None:
|
|
269
|
+
await current.on_resume(machine, now=now)
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
#
|
|
273
|
+
# Ticker
|
|
274
|
+
#
|
|
275
|
+
|
|
276
|
+
# Override
|
|
277
|
+
async def tick(self, now: Timestamp, elapsed: Duration):
|
|
278
|
+
if self.__status != MachineStatus.RUNNING:
|
|
279
|
+
# paused or stopped,
|
|
280
|
+
# cannot evaluate the transitions of current state
|
|
281
|
+
return
|
|
282
|
+
current = self.current_state
|
|
283
|
+
if current is not None:
|
|
284
|
+
machine = self.context
|
|
285
|
+
trans = current.evaluate(machine, now=now)
|
|
286
|
+
if trans is not None:
|
|
287
|
+
# assert isinstance(trans, BaseTransition), 'transition error: %s' % trans
|
|
288
|
+
target = self.get_target_state(transition=trans)
|
|
289
|
+
assert target is not None, 'target state error: %s' % trans.target
|
|
290
|
+
await self.__change_state(state=target, now=now)
|
small/fsm/machine.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Finite State Machine
|
|
4
|
+
#
|
|
5
|
+
# Written in 2021 by Moky <albert.moky@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# ==============================================================================
|
|
8
|
+
# MIT License
|
|
9
|
+
#
|
|
10
|
+
# Copyright (c) 2021 Albert Moky
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
# furnished to do so, subject to the following conditions:
|
|
18
|
+
#
|
|
19
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
# copies or substantial portions of the Software.
|
|
21
|
+
#
|
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
# SOFTWARE.
|
|
29
|
+
# ==============================================================================
|
|
30
|
+
|
|
31
|
+
from abc import ABC, abstractmethod
|
|
32
|
+
from typing import Optional, TypeVar, Generic
|
|
33
|
+
|
|
34
|
+
from ..utils import Timestamp
|
|
35
|
+
from ..skywalker import Ticker
|
|
36
|
+
|
|
37
|
+
S = TypeVar('S') # State
|
|
38
|
+
C = TypeVar('C') # Context
|
|
39
|
+
U = TypeVar('U')
|
|
40
|
+
T = TypeVar('T') # Transition
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Context(ABC):
|
|
44
|
+
""" State Machine Context """
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Transition(ABC, Generic[C]):
|
|
49
|
+
""" State Transition """
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def evaluate(self, ctx: C, now: Timestamp) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Evaluate the current state
|
|
55
|
+
|
|
56
|
+
:param ctx: context (machine)
|
|
57
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
58
|
+
:return True when current state should be changed
|
|
59
|
+
"""
|
|
60
|
+
raise NotImplementedError(
|
|
61
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.evaluate()'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class State(ABC, Generic[C, T]):
|
|
66
|
+
""" Finite State """
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def evaluate(self, ctx: C, now: Timestamp) -> Optional[T]:
|
|
70
|
+
"""
|
|
71
|
+
Called by machine.tick() to evaluate each transitions
|
|
72
|
+
|
|
73
|
+
:param ctx: context (machine)
|
|
74
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
75
|
+
:return success transition, or None to stay the current state
|
|
76
|
+
"""
|
|
77
|
+
raise NotImplementedError(
|
|
78
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.evaluate()'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def on_enter(self, old, ctx: C, now: Timestamp):
|
|
83
|
+
"""
|
|
84
|
+
Called after new state entered
|
|
85
|
+
|
|
86
|
+
:param old: previous state
|
|
87
|
+
:param ctx: context (machine)
|
|
88
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
89
|
+
"""
|
|
90
|
+
raise NotImplementedError(
|
|
91
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.on_enter()'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
async def on_exit(self, new, ctx: C, now: Timestamp):
|
|
96
|
+
"""
|
|
97
|
+
Called before old state exited
|
|
98
|
+
|
|
99
|
+
:param new: next state
|
|
100
|
+
:param ctx: context (machine)
|
|
101
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
102
|
+
"""
|
|
103
|
+
raise NotImplementedError(
|
|
104
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.on_exit()'
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
async def on_pause(self, ctx: C, now: Timestamp):
|
|
109
|
+
"""
|
|
110
|
+
Called before current state paused
|
|
111
|
+
|
|
112
|
+
:param ctx: context (machine)
|
|
113
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
114
|
+
"""
|
|
115
|
+
raise NotImplementedError(
|
|
116
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.on_pause()'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
async def on_resume(self, ctx: C, now: Timestamp):
|
|
121
|
+
"""
|
|
122
|
+
Called after current state resumed
|
|
123
|
+
|
|
124
|
+
:param ctx: context (machine)
|
|
125
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
126
|
+
"""
|
|
127
|
+
raise NotImplementedError(
|
|
128
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.on_resume()'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Delegate(ABC, Generic[C, T, S]):
|
|
133
|
+
""" State Machine Delegate """
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
async def enter_state(self, state: Optional[S], ctx: C, now: Timestamp):
|
|
137
|
+
"""
|
|
138
|
+
Called before enter new state
|
|
139
|
+
(get current state from context)
|
|
140
|
+
|
|
141
|
+
:param state: new state
|
|
142
|
+
:param ctx: context (machine)
|
|
143
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
144
|
+
"""
|
|
145
|
+
raise NotImplementedError(
|
|
146
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.enter_state()'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
async def exit_state(self, state: Optional[S], ctx: C, now: Timestamp):
|
|
151
|
+
"""
|
|
152
|
+
Called after exit old state
|
|
153
|
+
(get current state from context)
|
|
154
|
+
|
|
155
|
+
:param state: old state
|
|
156
|
+
:param ctx: context (machine)
|
|
157
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
158
|
+
"""
|
|
159
|
+
raise NotImplementedError(
|
|
160
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.exit_state()'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@abstractmethod
|
|
164
|
+
async def pause_state(self, state: Optional[S], ctx: C, now: Timestamp):
|
|
165
|
+
"""
|
|
166
|
+
Called after pause this state
|
|
167
|
+
|
|
168
|
+
:param state: current state
|
|
169
|
+
:param ctx: context (machine)
|
|
170
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
171
|
+
"""
|
|
172
|
+
raise NotImplementedError(
|
|
173
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.pause_state()'
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
async def resume_state(self, state: Optional[S], ctx: C, now: Timestamp):
|
|
178
|
+
"""
|
|
179
|
+
Called before resume this state
|
|
180
|
+
|
|
181
|
+
:param state: current state
|
|
182
|
+
:param ctx: context (machine)
|
|
183
|
+
:param now: current time (seconds from Jan 1, 1970 UTC)
|
|
184
|
+
"""
|
|
185
|
+
raise NotImplementedError(
|
|
186
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.resume_state()'
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class Machine(Ticker, ABC, Generic[C, T, S]):
|
|
191
|
+
""" State Machine """
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
@abstractmethod
|
|
195
|
+
def current_state(self) -> Optional[S]:
|
|
196
|
+
raise NotImplementedError(
|
|
197
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.current_state getter'
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@abstractmethod
|
|
201
|
+
async def start(self) -> bool:
|
|
202
|
+
""" Change current state to 'default' """
|
|
203
|
+
raise NotImplementedError(
|
|
204
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.start()'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@abstractmethod
|
|
208
|
+
async def stop(self) -> bool:
|
|
209
|
+
""" Change current state to null """
|
|
210
|
+
raise NotImplementedError(
|
|
211
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.stop()'
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
async def pause(self) -> bool:
|
|
216
|
+
""" Pause machine, current state not change """
|
|
217
|
+
raise NotImplementedError(
|
|
218
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.pause()'
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@abstractmethod
|
|
222
|
+
async def resume(self) -> bool:
|
|
223
|
+
""" Resume machine with current state """
|
|
224
|
+
raise NotImplementedError(
|
|
225
|
+
f'Not implemented: {type(self).__module__}.{type(self).__name__}.resume()'
|
|
226
|
+
)
|