qstate 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.
- qsm/__init__.py +10 -0
- qsm/deque.py +82 -0
- qsm/exceptions.py +4 -0
- qsm/qsm.py +72 -0
- qsm/states.py +30 -0
- qstate-1.0.0.dist-info/METADATA +816 -0
- qstate-1.0.0.dist-info/RECORD +9 -0
- qstate-1.0.0.dist-info/WHEEL +4 -0
- qstate-1.0.0.dist-info/licenses/LICENSE +674 -0
qsm/__init__.py
ADDED
qsm/deque.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from queue import Full, Queue
|
|
2
|
+
from time import monotonic
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StringDequeQueue(Queue[str]):
|
|
7
|
+
"""Thread-safe string queue that supports appending to either end.
|
|
8
|
+
|
|
9
|
+
This queue follows the synchronization behavior of ``queue.Queue`` while
|
|
10
|
+
adding prepend operations backed by the underlying deque.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def put(self, *items: str, block: bool = True, timeout: Optional[float] = None) -> None:
|
|
14
|
+
"""Append one or more strings using the standard ``Queue.put`` name."""
|
|
15
|
+
self.append(*items, block=block, timeout=timeout)
|
|
16
|
+
|
|
17
|
+
def append(self, *items: str, block: bool = True, timeout: Optional[float] = None) -> None:
|
|
18
|
+
"""Add one or more strings to the back of the queue."""
|
|
19
|
+
for item in items:
|
|
20
|
+
self._put_string(item, left=False, block=block, timeout=timeout)
|
|
21
|
+
|
|
22
|
+
def prepend(self, *items: str, block: bool = True, timeout: Optional[float] = None) -> None:
|
|
23
|
+
"""Add one or more strings to the front of the queue."""
|
|
24
|
+
for item in items:
|
|
25
|
+
self._put_string(item, left=True, block=block, timeout=timeout)
|
|
26
|
+
|
|
27
|
+
def append_nowait(self, *items: str) -> None:
|
|
28
|
+
"""Add one or more strings to the back without blocking."""
|
|
29
|
+
self.append(*items, block=False)
|
|
30
|
+
|
|
31
|
+
def prepend_nowait(self, *items: str) -> None:
|
|
32
|
+
"""Add one or more strings to the front without blocking."""
|
|
33
|
+
self.prepend(*items, block=False)
|
|
34
|
+
|
|
35
|
+
def flush(self) -> int:
|
|
36
|
+
"""Remove all queued strings and count them as completed.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The number of queued items removed.
|
|
40
|
+
"""
|
|
41
|
+
with self.mutex:
|
|
42
|
+
removed = self._qsize()
|
|
43
|
+
if removed == 0:
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
self.queue.clear()
|
|
47
|
+
self.unfinished_tasks -= removed
|
|
48
|
+
if self.unfinished_tasks == 0:
|
|
49
|
+
self.all_tasks_done.notify_all()
|
|
50
|
+
self.not_full.notify_all()
|
|
51
|
+
return removed
|
|
52
|
+
|
|
53
|
+
def _put_string(self, item: str, left: bool, block: bool, timeout: Optional[float]) -> None:
|
|
54
|
+
"""Add a single string while holding the queue's producer lock."""
|
|
55
|
+
if not isinstance(item, str):
|
|
56
|
+
raise TypeError("StringDequeQueue only accepts strings")
|
|
57
|
+
if timeout is not None and timeout < 0:
|
|
58
|
+
raise ValueError("'timeout' must be either 0 or non-negative")
|
|
59
|
+
|
|
60
|
+
with self.not_full:
|
|
61
|
+
if self.maxsize > 0:
|
|
62
|
+
if not block:
|
|
63
|
+
if self._qsize() >= self.maxsize:
|
|
64
|
+
raise Full
|
|
65
|
+
elif timeout is None:
|
|
66
|
+
while self._qsize() >= self.maxsize:
|
|
67
|
+
self.not_full.wait()
|
|
68
|
+
else:
|
|
69
|
+
endtime = monotonic() + timeout
|
|
70
|
+
while self._qsize() >= self.maxsize:
|
|
71
|
+
remaining = endtime - monotonic()
|
|
72
|
+
if remaining <= 0.0:
|
|
73
|
+
raise Full
|
|
74
|
+
self.not_full.wait(remaining)
|
|
75
|
+
|
|
76
|
+
if left:
|
|
77
|
+
self.queue.appendleft(item)
|
|
78
|
+
else:
|
|
79
|
+
self.queue.append(item)
|
|
80
|
+
|
|
81
|
+
self.unfinished_tasks += 1
|
|
82
|
+
self.not_empty.notify()
|
qsm/exceptions.py
ADDED
qsm/qsm.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from queue import Empty
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from .deque import StringDequeQueue
|
|
5
|
+
from .exceptions import NoSuchStateException
|
|
6
|
+
from .states import State, StateContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class QSM:
|
|
10
|
+
"""Queued state machine.
|
|
11
|
+
|
|
12
|
+
A ``QSM`` stores pending state names in a queue, resolves each name through
|
|
13
|
+
``state_map``, and executes states until no pending states remain.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, initial_context: Any = None, initial_state: str = "initial_state", max_queue_size: Optional[int] = None):
|
|
17
|
+
"""Create a queued state machine.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
initial_context: Shared workflow data passed to every state.
|
|
21
|
+
initial_state: State name used when ``loop`` is called without an
|
|
22
|
+
explicit initial state.
|
|
23
|
+
max_queue_size: Optional maximum number of queued states.
|
|
24
|
+
"""
|
|
25
|
+
self.queue = StringDequeQueue(maxsize=max_queue_size) if max_queue_size is not None else StringDequeQueue()
|
|
26
|
+
self.state_map: Dict[str, State] = {}
|
|
27
|
+
self.initial_state = initial_state
|
|
28
|
+
self.current_state: str = initial_state
|
|
29
|
+
self.context = initial_context
|
|
30
|
+
|
|
31
|
+
def get_next_state(self) -> Optional[str]:
|
|
32
|
+
"""Dequeues the next state name and makes it current.
|
|
33
|
+
|
|
34
|
+
Returns ``None`` when no state is queued.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
self.current_state = self.queue.get_nowait()
|
|
38
|
+
return self.current_state
|
|
39
|
+
except Empty:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def execute_state(self, name: str):
|
|
43
|
+
"""Execute a registered state by name.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
NoSuchStateException: If ``name`` is not registered in
|
|
47
|
+
``state_map``.
|
|
48
|
+
"""
|
|
49
|
+
if name not in self.state_map:
|
|
50
|
+
raise NoSuchStateException(name)
|
|
51
|
+
ctx = StateContext(
|
|
52
|
+
queue=self.queue,
|
|
53
|
+
context=self.context,
|
|
54
|
+
)
|
|
55
|
+
self.state_map[name].execute(ctx)
|
|
56
|
+
|
|
57
|
+
def execute_current_state(self):
|
|
58
|
+
"""Execute the state named by ``current_state``."""
|
|
59
|
+
self.execute_state(self.current_state)
|
|
60
|
+
|
|
61
|
+
def loop(self, flush: bool = True):
|
|
62
|
+
"""Run queued states until the queue is empty.
|
|
63
|
+
|
|
64
|
+
If ``flush`` is True, the loop will flush the current queue and enqueue self.initial_state
|
|
65
|
+
"""
|
|
66
|
+
if flush:
|
|
67
|
+
self.queue.flush()
|
|
68
|
+
self.queue.append(self.initial_state)
|
|
69
|
+
while not self.queue.empty():
|
|
70
|
+
state = self.get_next_state()
|
|
71
|
+
if state is not None:
|
|
72
|
+
self.execute_current_state()
|
qsm/states.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .deque import StringDequeQueue
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class StateContext:
|
|
13
|
+
"""Execution context passed into each state.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
queue: Queue used to schedule additional state names.
|
|
17
|
+
context: Shared workflow data owned by the ``QSM`` instance.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
queue: StringDequeQueue
|
|
21
|
+
context: Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class State(ABC):
|
|
25
|
+
"""Base class for queued state machine states."""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def execute(self, ctx: StateContext):
|
|
29
|
+
"""Run this state with the provided context."""
|
|
30
|
+
pass
|