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 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
+ )