python-statemachine 2.2.0__py3-none-any.whl → 2.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-statemachine
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Python Finite State Machines made easy.
5
5
  Home-page: https://github.com/fgmacedo/python-statemachine
6
6
  License: MIT
@@ -8,17 +8,18 @@ Author: Fernando Macedo
8
8
  Author-email: fgmacedo@gmail.com
9
9
  Maintainer: Fernando Macedo
10
10
  Maintainer-email: fgmacedo@gmail.com
11
- Requires-Python: >=3.9,<3.13
11
+ Requires-Python: >=3.7
12
+ Classifier: Framework :: AsyncIO
12
13
  Classifier: Intended Audience :: Developers
13
14
  Classifier: License :: OSI Approved :: MIT License
14
15
  Classifier: Natural Language :: English
15
16
  Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3.8
16
19
  Classifier: Programming Language :: Python :: 3.9
17
20
  Classifier: Programming Language :: Python :: 3.10
18
21
  Classifier: Programming Language :: Python :: 3.11
19
22
  Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.7
21
- Classifier: Programming Language :: Python :: 3.8
22
23
  Classifier: Topic :: Software Development :: Libraries
23
24
  Provides-Extra: diagrams
24
25
  Description-Content-Type: text/markdown
@@ -26,8 +27,8 @@ Description-Content-Type: text/markdown
26
27
  # Python StateMachine
27
28
 
28
29
  [![pypi](https://img.shields.io/pypi/v/python-statemachine.svg)](https://pypi.python.org/pypi/python-statemachine)
30
+ [![downloads total](https://static.pepy.tech/badge/python-statemachine)](https://pepy.tech/project/python-statemachine)
29
31
  [![downloads](https://img.shields.io/pypi/dm/python-statemachine.svg)](https://pypi.python.org/pypi/python-statemachine)
30
- [![build status](https://github.com/fgmacedo/python-statemachine/actions/workflows/python-package.yml/badge.svg?branch=develop)](https://github.com/fgmacedo/python-statemachine/actions/workflows/python-package.yml?query=branch%3Adevelop)
31
32
  [![Coverage report](https://codecov.io/gh/fgmacedo/python-statemachine/branch/develop/graph/badge.svg)](https://codecov.io/gh/fgmacedo/python-statemachine)
32
33
  [![Documentation Status](https://readthedocs.org/projects/python-statemachine/badge/?version=latest)](https://python-statemachine.readthedocs.io/en/latest/?badge=latest)
33
34
  [![GitHub commits since last release (main)](https://img.shields.io/github/commits-since/fgmacedo/python-statemachine/main/develop)](https://github.com/fgmacedo/python-statemachine/compare/main...develop)
@@ -35,33 +36,43 @@ Description-Content-Type: text/markdown
35
36
 
36
37
  Python [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machine) made easy.
37
38
 
39
+ <div align="center">
38
40
 
39
- * Free software: MIT license
40
- * Documentation: https://python-statemachine.readthedocs.io.
41
-
42
-
43
- Welcome to python-statemachine, an intuitive and powerful state machine framework designed for a
44
- great developer experience.
45
-
46
- 🚀 With StateMachine, you can easily create complex, dynamic systems with clean, readable code.
41
+ ![](https://github.com/fgmacedo/python-statemachine/blob/develop/docs/images/python-statemachine.png?raw=true)
47
42
 
48
- 💡 Our framework makes it easy to understand and reason about the different states, events and
49
- transitions in your system, so you can focus on building great products.
43
+ </div>
50
44
 
51
- 🔒 python-statemachine also provides robust error handling and ensures that your system stays
52
- in a valid state at all times.
45
+ Welcome to python-statemachine, an intuitive and powerful state machine library designed for a
46
+ great developer experience. We provide an _pythonic_ and expressive API for implementing state
47
+ machines in sync or asynchonous Python codebases.
53
48
 
49
+ ## Features
54
50
 
55
- A few reasons why you may consider using it:
51
+ - **Basic components**: Easily define **States**, **Events**, and **Transitions** to model your logic.
52
+ - ⚙️ **Actions and handlers**: Attach actions and handlers to states, events, and transitions to control behavior dynamically.
53
+ - 🛡️ **Conditional transitions**: Implement **Guards** and **Validators** to conditionally control transitions, ensuring they only occur when specific conditions are met.
54
+ - 🚀 **Full async support**: Enjoy full asynchronous support. Await events, and dispatch callbacks asynchronously for seamless integration with async codebases.
55
+ - 🔄 **Full sync support**: Use the same state machine from synchronous codebases without any modifications.
56
+ - 🎨 **Declarative and simple API**: Utilize a clean, elegant, and readable API to define your state machine, making it easy to maintain and understand.
57
+ - 👀 **Observer pattern support**: Register external and generic objects to watch events and register callbacks.
58
+ - 🔍 **Decoupled design**: Separate concerns with a decoupled "state machine" and "model" design, promoting cleaner architecture and easier maintenance.
59
+ - ✅ **Correctness guarantees**: Ensured correctness with validations at class definition time:
60
+ - Ensures exactly one `initial` state.
61
+ - Disallows transitions from `final` states.
62
+ - Requires ongoing transitions for all non-final states.
63
+ - Guarantees all non-final states have at least one path to a final state if final states are declared.
64
+ - Validates the state machine graph representation has a single component.
65
+ - 📦 **Flexible event dispatching**: Dispatch events with any extra data, making it available to all callbacks, including actions and guards.
66
+ - 🔧 **Dependency injection**: Needed parameters are injected into callbacks.
67
+ - 📊 **Graphical representation**: Generate and output graphical representations of state machines. Create diagrams from the command line, at runtime, or even in Jupyter notebooks.
68
+ - 🌍 **Internationalization support**: Provides error messages in different languages, making the library accessible to a global audience.
69
+ - 🛡️ **Robust testing**: Ensured reliability with a codebase that is 100% covered by automated tests, including all docs examples. Releases follow semantic versioning for predictable releases.
70
+ - 🏛️ **Domain model integration**: Seamlessly integrate with domain models using Mixins.
71
+ - 🔧 **Django integration**: Automatically discover state machines in Django applications.
56
72
 
57
- * 📈 python-statemachine is designed to help you build scalable,
58
- maintainable systems that can handle any complexity.
59
- * 💪 You can easily create and manage multiple state machines within a single application.
60
- * 🚫 Prevents common mistakes and ensures that your system stays in a valid state at all times.
61
73
 
62
74
 
63
- ## Getting started
64
-
75
+ ## Installing
65
76
 
66
77
  To install Python State Machine, run this command in your terminal:
67
78
 
@@ -73,6 +84,8 @@ our docs for more details.
73
84
 
74
85
  pip install python-statemachine[diagrams]
75
86
 
87
+ ## First example
88
+
76
89
  Define your state machine:
77
90
 
78
91
  ```py
@@ -90,7 +103,7 @@ Define your state machine:
90
103
  ... | red.to(green)
91
104
  ... )
92
105
  ...
93
- ... def before_cycle(self, event: str, source: State, target: State, message: str = ""):
106
+ ... async def before_cycle(self, event: str, source: State, target: State, message: str = ""):
94
107
  ... message = ". " + message if message else ""
95
108
  ... return f"Running {event} from {source.id} to {target.id}{message}"
96
109
  ...
@@ -133,7 +146,27 @@ Then start sending events to your new state machine:
133
146
 
134
147
  ```
135
148
 
136
- That's it. This is all an external object needs to know about your state machine: How to send events.
149
+ You can use the exactly same state machine from an async codebase:
150
+
151
+
152
+ ```py
153
+ >>> async def run_sm():
154
+ ... asm = TrafficLightMachine()
155
+ ... results = []
156
+ ... for _i in range(4):
157
+ ... result = await asm.send("cycle")
158
+ ... results.append(result)
159
+ ... return results
160
+
161
+ >>> asyncio.run(run_sm())
162
+ Don't move.
163
+ Go ahead!
164
+ ['Running cycle from green to yellow', 'Running cycle from yellow to red', ...
165
+
166
+ ```
167
+
168
+
169
+ **That's it.** This is all an external object needs to know about your state machine: How to send events.
137
170
  Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
138
171
 
139
172
  But if your use case needs, you can inspect state machine properties, like the current state:
@@ -220,7 +253,7 @@ callback method.
220
253
  Note how `before_cycle` was declared:
221
254
 
222
255
  ```py
223
- def before_cycle(self, event: str, source: State, target: State, message: str = ""):
256
+ async def before_cycle(self, event: str, source: State, target: State, message: str = ""):
224
257
  message = ". " + message if message else ""
225
258
  return f"Running {event} from {source.id} to {target.id}{message}"
226
259
  ```
@@ -265,6 +298,34 @@ and in diagrams:
265
298
 
266
299
  ```
267
300
 
301
+ ## Async support
302
+
303
+ We support native coroutine using `asyncio`, enabling seamless integration with asynchronous code.
304
+ There's no change on the public API of the library to work on async codebases.
305
+
306
+
307
+ ```py
308
+ >>> class AsyncStateMachine(StateMachine):
309
+ ... initial = State('Initial', initial=True)
310
+ ... final = State('Final', final=True)
311
+ ...
312
+ ... advance = initial.to(final)
313
+ ...
314
+ ... async def on_advance(self):
315
+ ... return 42
316
+
317
+ >>> async def run_sm():
318
+ ... sm = AsyncStateMachine()
319
+ ... result = await sm.advance()
320
+ ... print(f"Result is {result}")
321
+ ... print(sm.current_state)
322
+
323
+ >>> asyncio.run(run_sm())
324
+ Result is 42
325
+ Final
326
+
327
+ ```
328
+
268
329
  ## A more useful example
269
330
 
270
331
  A simple didactic state machine for controlling an `Order`:
@@ -0,0 +1,28 @@
1
+ statemachine/__init__.py,sha256=Omi4-IXrTE0oLfO3oF75OqMYBI9TrbZeBzZIkt-F0rU,192
2
+ statemachine/callbacks.py,sha256=D1mb3Ky18S1jk0-ursuJljqDv63WZShnMvMK_S1Hi9k,8846
3
+ statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ statemachine/contrib/diagram.py,sha256=CTlNRtAHEzCCzRUlxkiyzJOCOp1CohihdD0_7pHrx4c,7004
5
+ statemachine/dispatcher.py,sha256=Zizr_Qaj2BfuHu5VbbnNh1giT_pUVLe4mekHj6Ca2dk,5075
6
+ statemachine/event.py,sha256=lfJUSvkDQTnk1COueuV--xQD4o6GeaI94Y8tTOmiS6s,2304
7
+ statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
8
+ statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
9
+ statemachine/exceptions.py,sha256=RWq1vTjajxt8CzfYCD4DfI2nrgkfCPBL0DjRDOzKkQI,1086
10
+ statemachine/factory.py,sha256=pAfRUuO474FekxrUZkKr8NkQqQ59vOu4dRHSD8Dr9rI,7982
11
+ statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
12
+ statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
13
+ statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=zJYaV2DxrHdPngDOt1bji8-lPOZMSDjWG83Ypru5DOI,2126
14
+ statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=SPrt-KaCBCKQq7PoOMtyyqU1bihrw_3MPL_NbqZzTVs,3214
15
+ statemachine/mixins.py,sha256=B8WB3EGyZpMWAQg4Nw3kUMCfswgmLCChDpOQdxR28eE,1017
16
+ statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
17
+ statemachine/registry.py,sha256=RnVBRS3I_6Tm2OMgXMB_ewX7zQaslqEfhXFOhbqIkG4,959
18
+ statemachine/signature.py,sha256=aGoKxC36mTO60GGICVlyduhhB2nKVOXOOHepcBG2Uws,7809
19
+ statemachine/state.py,sha256=bZki0DyemZa-aVAfollQCmpXxesWONzUmE28eDfqz3o,8315
20
+ statemachine/statemachine.py,sha256=5r8vtbMFQtOq9W8UmaWBN_SIzOHkO-4JfPysHfdoB8w,13827
21
+ statemachine/states.py,sha256=gdGemJYF9k-cifs9Tk0Pe_-1u1Lanf3l08o0t8wAMgg,4203
22
+ statemachine/transition.py,sha256=fo6B-XlHDqU8TNrDXBSn4GStdGUrZbcfbEgtsdESmrk,5412
23
+ statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
24
+ statemachine/utils.py,sha256=FVtvT1lBSP3mRrYM-wxsX2pV_NZwSDsoBW4syIpACeE,798
25
+ python_statemachine-2.3.0.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
26
+ python_statemachine-2.3.0.dist-info/METADATA,sha256=ubb6vnMCYaYyz486RJW5r1dKKu_emWD7Rwc4x2S7lyY,14361
27
+ python_statemachine-2.3.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
28
+ python_statemachine-2.3.0.dist-info/RECORD,,
statemachine/__init__.py CHANGED
@@ -3,6 +3,6 @@ from .statemachine import StateMachine
3
3
 
4
4
  __author__ = """Fernando Macedo"""
5
5
  __email__ = "fgmacedo@gmail.com"
6
- __version__ = "2.2.0"
6
+ __version__ = "2.3.0"
7
7
 
8
8
  __all__ = ["StateMachine", "State"]
statemachine/callbacks.py CHANGED
@@ -20,7 +20,7 @@ class CallbackPriority(IntEnum):
20
20
  AFTER = 40
21
21
 
22
22
 
23
- def allways_true(*args, **kwargs):
23
+ async def allways_true(*args, **kwargs):
24
24
  return True
25
25
 
26
26
 
@@ -46,8 +46,8 @@ class CallbackWrapper:
46
46
  def __lt__(self, other):
47
47
  return self.meta.priority < other.meta.priority
48
48
 
49
- def __call__(self, *args, **kwargs):
50
- return self._callback(*args, **kwargs)
49
+ async def __call__(self, *args, **kwargs):
50
+ return await self._callback(*args, **kwargs)
51
51
 
52
52
 
53
53
  class CallbackMeta:
@@ -137,8 +137,8 @@ class BoolCallbackMeta(CallbackMeta):
137
137
  return name if self.expected_value else f"!{name}"
138
138
 
139
139
  def _wrap_callable(self, func, expected_value):
140
- def bool_wrapper(*args, **kwargs):
141
- return bool(func(*args, **kwargs)) == expected_value
140
+ async def bool_wrapper(*args, **kwargs):
141
+ return bool(await func(*args, **kwargs)) == expected_value
142
142
 
143
143
  return bool_wrapper
144
144
 
@@ -248,13 +248,18 @@ class CallbacksExecutor:
248
248
  self._add(item, resolver)
249
249
  return self
250
250
 
251
- def call(self, *args, **kwargs):
251
+ async def call(self, *args, **kwargs):
252
252
  return [
253
- callback(*args, **kwargs) for callback in self if callback.condition(*args, **kwargs)
253
+ await callback(*args, **kwargs)
254
+ for callback in self
255
+ if await callback.condition(*args, **kwargs)
254
256
  ]
255
257
 
256
- def all(self, *args, **kwargs):
257
- return all(condition(*args, **kwargs) for condition in self)
258
+ async def all(self, *args, **kwargs):
259
+ for condition in self:
260
+ if not await condition(*args, **kwargs):
261
+ return False
262
+ return True
258
263
 
259
264
 
260
265
  class CallbacksRegistry:
@@ -2,6 +2,7 @@ from collections import namedtuple
2
2
  from operator import attrgetter
3
3
  from typing import Any
4
4
  from typing import Generator
5
+ from typing import Tuple
5
6
 
6
7
  from .signature import SignatureAdapter
7
8
 
@@ -15,7 +16,7 @@ class ObjectConfig(namedtuple("ObjectConfig", "obj skip_attrs resolver_id")):
15
16
  """
16
17
 
17
18
  @classmethod
18
- def from_obj(cls, obj, skip_attrs=None):
19
+ def from_obj(cls, obj, skip_attrs=None) -> "ObjectConfig":
19
20
  if isinstance(obj, ObjectConfig):
20
21
  return obj
21
22
  else:
@@ -35,11 +36,11 @@ class WrapSearchResult:
35
36
  def wrap(self): # pragma: no cover
36
37
  pass
37
38
 
38
- def __call__(self, *args: Any, **kwds: Any) -> Any:
39
+ async def __call__(self, *args: Any, **kwds: Any) -> Any:
39
40
  if self._cache is None:
40
41
  self._cache = self.wrap()
41
42
  assert self._cache
42
- return self._cache(*args, **kwds)
43
+ return await self._cache(*args, **kwds)
43
44
 
44
45
 
45
46
  class CallableSearchResult(WrapSearchResult):
@@ -62,7 +63,7 @@ class AttributeCallableSearchResult(WrapSearchResult):
62
63
  # we'll build a method that get's the fresh value for each call
63
64
  getter = attrgetter(self.attribute)
64
65
 
65
- def wrapper(*args, **kwargs):
66
+ async def wrapper(*args, **kwargs):
66
67
  return getter(self.obj)
67
68
 
68
69
  return wrapper
@@ -76,15 +77,15 @@ class EventSearchResult(WrapSearchResult):
76
77
  def wrap(self):
77
78
  "Events already have the 'machine' parameter defined."
78
79
 
79
- def wrapper(*args, **kwargs):
80
+ async def wrapper(*args, **kwargs):
80
81
  kwargs.pop("machine", None)
81
- return self.func(*args, **kwargs)
82
+ return await self.func(*args, **kwargs)
82
83
 
83
84
  return wrapper
84
85
 
85
86
 
86
87
  def _search_callable_attr_is_property(
87
- attr, configs: tuple[ObjectConfig]
88
+ attr, configs: Tuple[ObjectConfig, ...]
88
89
  ) -> "WrapSearchResult | None":
89
90
  # if the attr is a property, we'll try to find the object that has the
90
91
  # property on the configs
@@ -96,7 +97,7 @@ def _search_callable_attr_is_property(
96
97
  return None
97
98
 
98
99
 
99
- def _search_callable_attr_is_callable(attr, configs: tuple[ObjectConfig]) -> WrapSearchResult:
100
+ def _search_callable_attr_is_callable(attr, configs: Tuple[ObjectConfig, ...]) -> WrapSearchResult:
100
101
  # if the attr is an unbounded method, we'll try to find the bounded method
101
102
  # on the configs
102
103
  if not hasattr(attr, "__self__"):
@@ -109,7 +110,7 @@ def _search_callable_attr_is_callable(attr, configs: tuple[ObjectConfig]) -> Wra
109
110
 
110
111
 
111
112
  def _search_callable_in_configs(
112
- attr, configs: tuple[ObjectConfig]
113
+ attr, configs: Tuple[ObjectConfig, ...]
113
114
  ) -> Generator[WrapSearchResult, None, None]:
114
115
  for obj, skip_attrs, resolver_id in configs:
115
116
  if attr in skip_attrs:
@@ -128,7 +129,9 @@ def _search_callable_in_configs(
128
129
  yield CallableSearchResult(attr, func, resolver_id)
129
130
 
130
131
 
131
- def search_callable(attr, configs: tuple) -> Generator[WrapSearchResult, None, None]: # noqa: C901
132
+ def search_callable(
133
+ attr, configs: Tuple[ObjectConfig, ...]
134
+ ) -> Generator[WrapSearchResult, None, None]: # noqa: C901
132
135
  if isinstance(attr, property):
133
136
  result = _search_callable_attr_is_property(attr, configs)
134
137
  if result is not None:
@@ -142,15 +145,15 @@ def search_callable(attr, configs: tuple) -> Generator[WrapSearchResult, None, N
142
145
  yield from _search_callable_in_configs(attr, configs)
143
146
 
144
147
 
145
- def resolver_factory(objects: tuple[ObjectConfig]):
148
+ def resolver_factory(objects: Tuple[ObjectConfig, ...]):
146
149
  """Factory that returns a configured resolver."""
147
150
 
148
- def wrapper(attr) -> Generator[WrapSearchResult, None, None]:
151
+ def resolver(attr) -> Generator[WrapSearchResult, None, None]:
149
152
  yield from search_callable(attr, objects)
150
153
 
151
- return wrapper
154
+ return resolver
152
155
 
153
156
 
154
- def resolver_factory_from_objects(*objects: tuple[Any]):
155
- configs = tuple(ObjectConfig.from_obj(o) for o in objects)
157
+ def resolver_factory_from_objects(*objects: Tuple[Any, ...]):
158
+ configs: Tuple[ObjectConfig, ...] = tuple(ObjectConfig.from_obj(o) for o in objects)
156
159
  return resolver_factory(configs)
statemachine/event.py CHANGED
@@ -1,5 +1,8 @@
1
+ from functools import partial
1
2
  from typing import TYPE_CHECKING
2
3
 
4
+ from statemachine.utils import run_async_from_sync
5
+
3
6
  from .event_data import EventData
4
7
  from .event_data import TriggerData
5
8
  from .exceptions import TransitionNotAllowed
@@ -15,28 +18,28 @@ class Event:
15
18
  def __repr__(self):
16
19
  return f"{type(self).__name__}({self.name!r})"
17
20
 
18
- def trigger(self, machine: "StateMachine", *args, **kwargs):
19
- def trigger_wrapper():
20
- """Wrapper that captures event_data as closure."""
21
- trigger_data = TriggerData(
22
- machine=machine,
23
- event=self.name,
24
- args=args,
25
- kwargs=kwargs,
26
- )
27
- return self._trigger(trigger_data)
21
+ async def trigger(self, machine: "StateMachine", *args, **kwargs):
22
+ trigger_data = TriggerData(
23
+ machine=machine,
24
+ event=self.name,
25
+ args=args,
26
+ kwargs=kwargs,
27
+ )
28
+ trigger_wrapper = partial(self._trigger, trigger_data=trigger_data)
28
29
 
29
- return machine._process(trigger_wrapper)
30
+ return await machine._process(trigger_wrapper)
30
31
 
31
- def _trigger(self, trigger_data: TriggerData):
32
+ async def _trigger(self, trigger_data: TriggerData):
32
33
  event_data = None
34
+ await trigger_data.machine._ensure_is_initialized()
35
+
33
36
  state = trigger_data.machine.current_state
34
37
  for transition in state.transitions:
35
38
  if not transition.match(trigger_data.event):
36
39
  continue
37
40
 
38
41
  event_data = EventData(trigger_data=trigger_data, transition=transition)
39
- if transition.execute(event_data):
42
+ if await transition.execute(event_data):
40
43
  event_data.executed = True
41
44
  break
42
45
  else:
@@ -46,17 +49,15 @@ class Event:
46
49
  return event_data.result if event_data else None
47
50
 
48
51
 
49
- def trigger_event_factory(event):
52
+ def trigger_event_factory(event_instance: Event):
50
53
  """Build a method that sends specific `event` to the machine"""
51
- event_instance = Event(event)
52
54
 
53
55
  def trigger_event(self, *args, **kwargs):
54
- return event_instance.trigger(self, *args, **kwargs)
55
-
56
- trigger_event.name = event
57
- trigger_event.identifier = event
58
- trigger_event._is_sm_event = True
56
+ return run_async_from_sync(event_instance.trigger(self, *args, **kwargs))
59
57
 
58
+ trigger_event.name = event_instance.name # type: ignore[attr-defined]
59
+ trigger_event.identifier = event_instance.name # type: ignore[attr-defined]
60
+ trigger_event._is_sm_event = True # type: ignore[attr-defined]
60
61
  return trigger_event
61
62
 
62
63
 
@@ -1,5 +1,10 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  from .i18n import _
2
4
 
5
+ if TYPE_CHECKING:
6
+ from .state import State
7
+
3
8
 
4
9
  class StateMachineError(Exception):
5
10
  "Base exception for this project, all exceptions that can be raised inherit from this class."
@@ -12,9 +17,10 @@ class InvalidDefinition(StateMachineError):
12
17
  class InvalidStateValue(InvalidDefinition):
13
18
  "The current model state value is not mapped to a state definition."
14
19
 
15
- def __init__(self, value):
20
+ def __init__(self, value, msg=None):
16
21
  self.value = value
17
- msg = _("{!r} is not a valid state value.").format(value)
22
+ if msg is None:
23
+ msg = _("{!r} is not a valid state value.").format(value)
18
24
  super().__init__(msg)
19
25
 
20
26
 
@@ -25,7 +31,7 @@ class AttrNotFound(InvalidDefinition):
25
31
  class TransitionNotAllowed(StateMachineError):
26
32
  "Raised when there's no transition that can run from the current :ref:`state`."
27
33
 
28
- def __init__(self, event, state):
34
+ def __init__(self, event: str, state: "State"):
29
35
  self.event = event
30
36
  self.state = state
31
37
  msg = _("Can't {} when in {}.").format(self.event, self.state.name)
statemachine/factory.py CHANGED
@@ -222,8 +222,7 @@ class StateMachineMetaclass(type):
222
222
  if event not in cls._events:
223
223
  event_instance = Event(event)
224
224
  cls._events[event] = event_instance
225
- event_trigger = trigger_event_factory(event)
226
- setattr(cls, event, event_trigger)
225
+ setattr(cls, event, trigger_event_factory(event_instance))
227
226
 
228
227
  return cls._events[event]
229
228
 
@@ -1,66 +1,80 @@
1
1
  # This file is distributed under the same license as the PROJECT project.
2
- # Fernando Macedo <fgmacedo@gmail.com>, 2023.
2
+ # Fernando Macedo <fgmacedo@gmail.com>, 2024.
3
3
  #
4
4
  msgid ""
5
5
  msgstr ""
6
- "Project-Id-Version: 2.1.0\n"
6
+ "Project-Id-Version: 2.3.0\n"
7
7
  "Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
8
8
  "POT-Creation-Date: 2023-03-04 16:10-0300\n"
9
- "PO-Revision-Date: 2023-03-04 16:10-0300\n"
9
+ "PO-Revision-Date: 2024-06-07 17:41-0300\n"
10
10
  "Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
11
11
  "MIME-Version: 1.0\n"
12
12
  "Content-Type: text/plain; charset=utf-8\n"
13
13
  "Content-Transfer-Encoding: 8bit\n"
14
14
  "Generated-By: Babel 2.12.1\n"
15
15
 
16
- #: statemachine/callbacks.py:56
17
- msgid "Callback {!r} not property configured."
18
- msgstr ""
19
-
20
- #: statemachine/dispatcher.py:35
16
+ #: statemachine/callbacks.py:289
21
17
  msgid "Did not found name '{}' from model or statemachine"
22
18
  msgstr ""
23
19
 
24
- #: statemachine/exceptions.py:17
20
+ #: statemachine/exceptions.py:23
25
21
  msgid "{!r} is not a valid state value."
26
22
  msgstr ""
27
23
 
28
- #: statemachine/exceptions.py:31
24
+ #: statemachine/exceptions.py:37
29
25
  msgid "Can't {} when in {}."
30
26
  msgstr ""
31
27
 
32
- #: statemachine/factory.py:35
33
- msgid ""
34
- "There should be one and only one initial state. Your currently have "
35
- "these: {!r}"
28
+ #: statemachine/factory.py:73
29
+ msgid "There are no states."
30
+ msgstr ""
31
+
32
+ #: statemachine/factory.py:76
33
+ msgid "There are no events."
36
34
  msgstr ""
37
35
 
38
- #: statemachine/factory.py:51
36
+ #: statemachine/factory.py:88
39
37
  msgid ""
40
- "There are unreachable states. The statemachine graph should have a single"
41
- " component. Disconnected states: {}"
38
+ "There should be one and only one initial state. You currently have these:"
39
+ " {!r}"
42
40
  msgstr ""
43
41
 
44
- #: statemachine/factory.py:69
45
- msgid "There are no states."
42
+ #: statemachine/factory.py:101
43
+ msgid "Cannot declare transitions from final state. Invalid state(s): {}"
46
44
  msgstr ""
47
45
 
48
- #: statemachine/factory.py:72
49
- msgid "There are no events."
46
+ #: statemachine/factory.py:109
47
+ msgid ""
48
+ "All non-final states should have at least one outgoing transition. These "
49
+ "states have no outgoing transition: {!r}"
50
50
  msgstr ""
51
51
 
52
- #: statemachine/factory.py:82
53
- msgid "Cannot declare transitions from final state. Invalid state(s): {}"
52
+ #: statemachine/factory.py:123
53
+ msgid ""
54
+ "All non-final states should have at least one path to a final state. "
55
+ "These states have no path to a final state: {!r}"
54
56
  msgstr ""
55
57
 
56
- #: statemachine/mixins.py:30
58
+ #: statemachine/factory.py:147
59
+ msgid ""
60
+ "There are unreachable states. The statemachine graph should have a single"
61
+ " component. Disconnected states: {}"
62
+ msgstr ""
63
+
64
+ #: statemachine/mixins.py:23
57
65
  msgid "{!r} is not a valid state machine name."
58
66
  msgstr ""
59
67
 
60
- #: statemachine/state.py:148
68
+ #: statemachine/state.py:152
61
69
  msgid "State overriding is not allowed. Trying to add '{}' to {}"
62
70
  msgstr ""
63
71
 
64
- #: statemachine/statemachine.py:48
72
+ #: statemachine/statemachine.py:86
65
73
  msgid "There are no states or transitions."
66
74
  msgstr ""
75
+
76
+ #: statemachine/statemachine.py:249
77
+ msgid ""
78
+ "There's no current state set. In async code, did you activate the initial"
79
+ " state? (e.g., `await sm.activate_initial_state()`)"
80
+ msgstr ""
@@ -1,70 +1,91 @@
1
- # This file is distributed under the same license as the PROJECT project.
2
- # Fernando Macedo <fgmacedo@gmail.com>, 2023.
1
+ # This file is distributed under the same license as the project.
2
+ # Fernando Macedo <fgmacedo@gmail.com>, 2024.
3
3
  #
4
4
  msgid ""
5
5
  msgstr ""
6
- "Project-Id-Version: 2.1.0\n"
6
+ "Project-Id-Version: 2.3.0\n"
7
7
  "Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
8
8
  "POT-Creation-Date: 2023-03-04 16:10-0300\n"
9
- "PO-Revision-Date: 2023-03-04 16:10-0300\n"
9
+ "PO-Revision-Date: 2024-06-07 17:41-0300\n"
10
10
  "Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
11
+ "Language-Team: LANGUAGE <LL@li.org>\n"
11
12
  "MIME-Version: 1.0\n"
12
13
  "Content-Type: text/plain; charset=utf-8\n"
13
14
  "Content-Transfer-Encoding: 8bit\n"
14
- "Generated-By: Babel 2.12.1\n"
15
+ "Generated-By: Babel 2.14.0\n"
15
16
 
16
- #: statemachine/callbacks.py:56
17
- msgid "Callback {!r} not property configured."
18
- msgstr "Callback {!r} não está configurado corretamente."
19
-
20
- #: statemachine/dispatcher.py:35
17
+ #: statemachine/callbacks.py:289
21
18
  msgid "Did not found name '{}' from model or statemachine"
22
- msgstr "Não foi encontrado o nome '{}' no modelo ou na máquina de estados"
19
+ msgstr "Não encontrou o nome '{}' no modelo ou na máquina de estados"
23
20
 
24
- #: statemachine/exceptions.py:17
21
+ #: statemachine/exceptions.py:23
25
22
  msgid "{!r} is not a valid state value."
26
23
  msgstr "{!r} não é um valor de estado válido."
27
24
 
28
- #: statemachine/exceptions.py:31
25
+ #: statemachine/exceptions.py:37
29
26
  msgid "Can't {} when in {}."
30
- msgstr "Não é possível {} quando em {}."
31
-
32
- #: statemachine/factory.py:35
33
- msgid ""
34
- "There should be one and only one initial state. Your currently have "
35
- "these: {!r}"
36
- msgstr ""
37
- "Deve haver um e apenas um estado inicial. Atualmente, você tem "
38
- "os seguintes: {!r}"
27
+ msgstr "Não é possível {} quando está em {}."
39
28
 
40
- #: statemachine/factory.py:51
41
- msgid ""
42
- "There are unreachable states. The statemachine graph should have a single"
43
- " component. Disconnected states: {}"
44
- msgstr ""
45
- "Há estados inalcançáveis. O grafo da máquina de estados deve ter apenas um"
46
- " componente. Estados desconectados: {}"
47
-
48
- #: statemachine/factory.py:69
29
+ #: statemachine/factory.py:73
49
30
  msgid "There are no states."
50
31
  msgstr "Não há estados."
51
32
 
52
- #: statemachine/factory.py:72
33
+ #: statemachine/factory.py:76
53
34
  msgid "There are no events."
54
35
  msgstr "Não há eventos."
55
36
 
56
- #: statemachine/factory.py:82
37
+ #: statemachine/factory.py:88
38
+ msgid ""
39
+ "There should be one and only one initial state. You currently have these:"
40
+ " {!r}"
41
+ msgstr "Deve haver um e apenas um estado inicial. Você atualmente tem estes: {!r}"
42
+
43
+ #: statemachine/factory.py:101
57
44
  msgid "Cannot declare transitions from final state. Invalid state(s): {}"
58
- msgstr "Não é possível declarar transições a partir do estado final. Estado(s) inválido(s): {}"
45
+ msgstr ""
46
+ "Não é possível declarar transições a partir do estado final. Estado(s) "
47
+ "inválido(s): {}"
48
+
49
+ #: statemachine/factory.py:109
50
+ msgid ""
51
+ "All non-final states should have at least one outgoing transition. These "
52
+ "states have no outgoing transition: {!r}"
53
+ msgstr ""
54
+ "Todos os estados não finais devem ter pelo menos uma transição de saída. "
55
+ "Esses estados não têm transição de saída: {!r}"
59
56
 
60
- #: statemachine/mixins.py:30
57
+ #: statemachine/factory.py:123
58
+ msgid ""
59
+ "All non-final states should have at least one path to a final state. "
60
+ "These states have no path to a final state: {!r}"
61
+ msgstr ""
62
+ "Todos os estados não finais devem ter pelo menos um caminho para um "
63
+ "estado final. Esses estados não têm caminho para um estado final: {!r}"
64
+
65
+ #: statemachine/factory.py:147
66
+ msgid ""
67
+ "There are unreachable states. The statemachine graph should have a single"
68
+ " component. Disconnected states: {}"
69
+ msgstr ""
70
+ "Há estados inacessíveis. O gráfico da máquina de estados deve ter um "
71
+ "único componente. Estados desconectados: {}"
72
+
73
+ #: statemachine/mixins.py:23
61
74
  msgid "{!r} is not a valid state machine name."
62
- msgstr "{!r} não é um nome válido para a máquina de estados."
75
+ msgstr "{!r} não é um nome válido para uma máquina de estados."
63
76
 
64
- #: statemachine/state.py:148
77
+ #: statemachine/state.py:152
65
78
  msgid "State overriding is not allowed. Trying to add '{}' to {}"
66
- msgstr "A substituição de estado não é permitida. Tentando adicionar '{}' a {}"
79
+ msgstr "Sobrescrever estados não é permitido. Tentando adicionar '{}' a {}"
67
80
 
68
- #: statemachine/statemachine.py:48
81
+ #: statemachine/statemachine.py:86
69
82
  msgid "There are no states or transitions."
70
83
  msgstr "Não há estados ou transições."
84
+
85
+ #: statemachine/statemachine.py:249
86
+ msgid ""
87
+ "There's no current state set. In async code, did you activate the initial"
88
+ " state? (e.g., `await sm.activate_initial_state()`)"
89
+ msgstr ""
90
+ "Não há estado atual definido. No código assíncrono, você ativou o estado"
91
+ " inicial? (por exemplo, `await sm.activate_initial_state()`)"
statemachine/signature.py CHANGED
@@ -1,8 +1,9 @@
1
- import itertools
2
1
  from functools import partial
3
2
  from inspect import BoundArguments
4
3
  from inspect import Parameter
5
4
  from inspect import Signature
5
+ from inspect import iscoroutinefunction
6
+ from itertools import chain
6
7
  from types import MethodType
7
8
  from typing import Any
8
9
 
@@ -51,9 +52,16 @@ class SignatureAdapter(Signature):
51
52
 
52
53
  metadata_to_copy = method.func if isinstance(method, partial) else method
53
54
 
54
- def method_wrapper(*args: Any, **kwargs: Any) -> Any:
55
- ba = sig_bind_expected(*args, **kwargs)
56
- return method(*ba.args, **ba.kwargs)
55
+ if iscoroutinefunction(method):
56
+
57
+ async def method_wrapper(*args: Any, **kwargs: Any) -> Any:
58
+ ba = sig_bind_expected(*args, **kwargs)
59
+ return await method(*ba.args, **ba.kwargs)
60
+ else:
61
+
62
+ async def method_wrapper(*args: Any, **kwargs: Any) -> Any:
63
+ ba = sig_bind_expected(*args, **kwargs)
64
+ return method(*ba.args, **ba.kwargs)
57
65
 
58
66
  method_wrapper.__name__ = metadata_to_copy.__name__
59
67
 
@@ -160,7 +168,7 @@ class SignatureAdapter(Signature):
160
168
 
161
169
  # Now, we iterate through the remaining parameters to process
162
170
  # keyword arguments
163
- for param in itertools.chain(parameters_ex, parameters):
171
+ for param in chain(parameters_ex, parameters):
164
172
  if param.kind == Parameter.VAR_KEYWORD:
165
173
  # Memorize that we have a '**kwargs'-like parameter
166
174
  kwargs_param = param
@@ -6,6 +6,7 @@ from typing import Any
6
6
  from typing import Dict
7
7
 
8
8
  from statemachine.graph import iterate_states_and_transitions
9
+ from statemachine.utils import run_async_from_sync
9
10
 
10
11
  from .callbacks import CallbackMetaList
11
12
  from .callbacks import CallbacksExecutor
@@ -73,18 +74,23 @@ class StateMachine(metaclass=StateMachineMetaclass):
73
74
  self.state_field = state_field
74
75
  self.start_value = start_value
75
76
  self.allow_event_without_transition = allow_event_without_transition
77
+ self.__initialized = False
76
78
  self.__rtc = rtc
77
79
  self.__processing: bool = False
78
80
  self._external_queue: deque = deque()
79
81
  self._callbacks_registry = CallbacksRegistry()
80
- self._states_for_instance: Dict["State", "State"] = {}
82
+ self._states_for_instance: Dict[State, State] = {}
81
83
  self._observers: Dict[Any, Any] = {}
82
84
 
83
85
  if self._abstract:
84
86
  raise InvalidDefinition(_("There are no states or transitions."))
85
87
 
86
88
  self._register_callbacks()
87
- self._activate_initial_state()
89
+
90
+ # Activate the initial state, this only works if the outer scope is sync code.
91
+ # for async code, the user should manually call `await sm.activate_initial_state()`
92
+ # after state machine creation.
93
+ run_async_from_sync(self.activate_initial_state())
88
94
 
89
95
  def __init_subclass__(cls, strict_states: bool = False):
90
96
  cls._strict_states = strict_states
@@ -122,7 +128,23 @@ class StateMachine(metaclass=StateMachineMetaclass):
122
128
  except KeyError as err:
123
129
  raise InvalidStateValue(current_state_value) from err
124
130
 
125
- def _activate_initial_state(self):
131
+ async def activate_initial_state(self):
132
+ """
133
+ Activate the initial state.
134
+
135
+ Called automatically on state machine creation from sync code, but in
136
+ async code, the user must call this method explicitly.
137
+
138
+ Given how async works on python, there's no built-in way to activate the initial state that
139
+ may depend on async code from the StateMachine.__init__ method.
140
+
141
+ We do a `_ensure_is_initialized()` check before each event, but to check the current state
142
+ just before the state machine is created, the user must await the activation of the initial
143
+ state explicitly.
144
+ """
145
+ if self.__initialized:
146
+ return
147
+ self.__initialized = True
126
148
  if self.current_state_value is None:
127
149
  # send an one-time event `__initial__` to enter the current state.
128
150
  # current_state = self.current_state
@@ -138,7 +160,10 @@ class StateMachine(metaclass=StateMachineMetaclass):
138
160
  ),
139
161
  transition=initial_transition,
140
162
  )
141
- self._activate(event_data)
163
+ await self._activate(event_data)
164
+
165
+ async def _ensure_is_initialized(self):
166
+ await self.activate_initial_state()
142
167
 
143
168
  def _register_callbacks(self):
144
169
  self._add_observer(
@@ -195,8 +220,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
195
220
  This is a low level API, that can be used to assign any valid state value
196
221
  completely bypassing all the hooks and validations.
197
222
  """
198
- value = getattr(self.model, self.state_field, None)
199
- return value
223
+ return getattr(self.model, self.state_field, None)
200
224
 
201
225
  @current_state_value.setter
202
226
  def current_state_value(self, value):
@@ -212,11 +236,23 @@ class StateMachine(metaclass=StateMachineMetaclass):
212
236
  completely bypassing all the hooks and validations.
213
237
  """
214
238
 
215
- state: State = self.states_map[self.current_state_value].for_instance(
216
- machine=self,
217
- cache=self._states_for_instance,
218
- )
219
- return state
239
+ try:
240
+ state: State = self.states_map[self.current_state_value].for_instance(
241
+ machine=self,
242
+ cache=self._states_for_instance,
243
+ )
244
+ return state
245
+ except KeyError as err:
246
+ if self.current_state_value is None:
247
+ raise InvalidStateValue(
248
+ self.current_state_value,
249
+ _(
250
+ "There's no current state set. In async code, "
251
+ "did you activate the initial state? "
252
+ "(e.g., `await sm.activate_initial_state()`)"
253
+ ),
254
+ ) from err
255
+ raise InvalidStateValue(self.current_state_value) from err
220
256
 
221
257
  @current_state.setter
222
258
  def current_state(self, value):
@@ -231,7 +267,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
231
267
  """List of the current allowed events."""
232
268
  return [getattr(self, event) for event in self.current_state.transitions.unique_events]
233
269
 
234
- def _process(self, trigger):
270
+ async def _process(self, trigger):
235
271
  """Process event triggers.
236
272
 
237
273
  The simplest implementation is the non-RTC (synchronous),
@@ -252,7 +288,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
252
288
  """
253
289
  if not self.__rtc:
254
290
  # The machine is in "synchronous" mode
255
- return trigger()
291
+ return await trigger()
256
292
 
257
293
  # The machine is in "queued" mode
258
294
  # Add the trigger to queue and start processing in a loop.
@@ -263,9 +299,9 @@ class StateMachine(metaclass=StateMachineMetaclass):
263
299
  if self.__processing:
264
300
  return
265
301
 
266
- return self._processing_loop()
302
+ return await self._processing_loop()
267
303
 
268
- def _processing_loop(self):
304
+ async def _processing_loop(self):
269
305
  """Execute the triggers in the queue in order until the queue is empty"""
270
306
  self.__processing = True
271
307
 
@@ -278,7 +314,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
278
314
  while self._external_queue:
279
315
  trigger = self._external_queue.popleft()
280
316
  try:
281
- result = trigger()
317
+ result = await trigger()
282
318
  if first_result is sentinel:
283
319
  first_result = result
284
320
  except Exception:
@@ -290,18 +326,18 @@ class StateMachine(metaclass=StateMachineMetaclass):
290
326
  self.__processing = False
291
327
  return first_result if first_result is not sentinel else None
292
328
 
293
- def _activate(self, event_data: EventData):
329
+ async def _activate(self, event_data: EventData):
294
330
  transition = event_data.transition
295
331
  source = event_data.state
296
332
  target = transition.target
297
333
 
298
- result = self._callbacks(transition.before).call(
334
+ result = await self._callbacks(transition.before).call(
299
335
  *event_data.args, **event_data.extended_kwargs
300
336
  )
301
337
  if source is not None and not transition.internal:
302
- self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
338
+ await self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
303
339
 
304
- result += self._callbacks(transition.on).call(
340
+ result += await self._callbacks(transition.on).call(
305
341
  *event_data.args, **event_data.extended_kwargs
306
342
  )
307
343
 
@@ -309,8 +345,12 @@ class StateMachine(metaclass=StateMachineMetaclass):
309
345
  event_data.state = target
310
346
 
311
347
  if not transition.internal:
312
- self._callbacks(target.enter).call(*event_data.args, **event_data.extended_kwargs)
313
- self._callbacks(transition.after).call(*event_data.args, **event_data.extended_kwargs)
348
+ await self._callbacks(target.enter).call(
349
+ *event_data.args, **event_data.extended_kwargs
350
+ )
351
+ await self._callbacks(transition.after).call(
352
+ *event_data.args, **event_data.extended_kwargs
353
+ )
314
354
 
315
355
  if len(result) == 0:
316
356
  result = None
@@ -319,7 +359,20 @@ class StateMachine(metaclass=StateMachineMetaclass):
319
359
 
320
360
  return result
321
361
 
322
- def send(self, event, *args, **kwargs):
362
+ def send(self, event: str, *args, **kwargs):
363
+ """Send an :ref:`Event` to the state machine.
364
+
365
+ This is a thin wrapper around :meth:`async_send` to allow synchronous
366
+ code to send events.
367
+
368
+ .. seealso::
369
+
370
+ See: :ref:`triggering events`.
371
+
372
+ """
373
+ return run_async_from_sync(self.async_send(event, *args, **kwargs))
374
+
375
+ async def async_send(self, event: str, *args, **kwargs):
323
376
  """Send an :ref:`Event` to the state machine.
324
377
 
325
378
  .. seealso::
@@ -327,8 +380,8 @@ class StateMachine(metaclass=StateMachineMetaclass):
327
380
  See: :ref:`triggering events`.
328
381
 
329
382
  """
330
- event = Event(event)
331
- return event.trigger(self, *args, **kwargs)
383
+ event_instance: Event = Event(event)
384
+ return await event_instance.trigger(self, *args, **kwargs)
332
385
 
333
386
  def _callbacks(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
334
387
  return self._callbacks_registry[meta_list]
@@ -139,13 +139,13 @@ class Transition:
139
139
  def add_event(self, value):
140
140
  self._events.add(value)
141
141
 
142
- def execute(self, event_data: "EventData"):
142
+ async def execute(self, event_data: "EventData"):
143
143
  machine = event_data.machine
144
144
  args, kwargs = event_data.args, event_data.extended_kwargs
145
- machine._callbacks_registry[self.validators].call(*args, **kwargs)
146
- if not machine._callbacks_registry[self.cond].all(*args, **kwargs):
145
+ await machine._callbacks_registry[self.validators].call(*args, **kwargs)
146
+ if not await machine._callbacks_registry[self.cond].all(*args, **kwargs):
147
147
  return False
148
148
 
149
- result = machine._activate(event_data)
149
+ result = await machine._activate(event_data)
150
150
  event_data.result = result
151
151
  return True
statemachine/utils.py CHANGED
@@ -1,3 +1,6 @@
1
+ import asyncio
2
+
3
+
1
4
  def qualname(cls):
2
5
  """
3
6
  Returns a fully qualified name of the class, to avoid name collisions.
@@ -16,3 +19,15 @@ def ensure_iterable(obj):
16
19
  return iter(obj)
17
20
  except TypeError:
18
21
  return [obj]
22
+
23
+
24
+ def run_async_from_sync(coroutine):
25
+ """
26
+ Run an async coroutine from a synchronous context.
27
+ """
28
+ try:
29
+ loop = asyncio.get_running_loop()
30
+ return asyncio.ensure_future(coroutine)
31
+ except RuntimeError:
32
+ loop = asyncio.get_event_loop()
33
+ return loop.run_until_complete(coroutine)
@@ -1,28 +0,0 @@
1
- statemachine/__init__.py,sha256=nhqSQwRfWBVrcn4h-aLlxLbzeSsjt6lE_SSFnFWK0FY,192
2
- statemachine/callbacks.py,sha256=QMh564YuNVNV74VxlIyvlF_eW6njsmV9VHauQH9JR1U,8704
3
- statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- statemachine/contrib/diagram.py,sha256=CTlNRtAHEzCCzRUlxkiyzJOCOp1CohihdD0_7pHrx4c,7004
5
- statemachine/dispatcher.py,sha256=i9ChhdomQpVbTg9vNS3xoCHUAw3Z32B0UPzK47nb1-A,4924
6
- statemachine/event.py,sha256=fvWxyFcrbPVxNW94p66rGJ5I1UZUK9mDJz68udkz2JI,2106
7
- statemachine/event_data.py,sha256=nxOfypngK-78A77gj4pS-oxLx9zl1S9wnSt_rTiCyZA,2257
8
- statemachine/events.py,sha256=rUbiTX-QGoo7zzmQMEeWeLrIVnp9zhicdRIm34CoAVI,731
9
- statemachine/exceptions.py,sha256=uzvDbHkzCZkmFI_02L0yC9gd6d0SBn9GmsI8ekc0Hjk,952
10
- statemachine/factory.py,sha256=m2TeJ19eDIymoGADDp0G0ItrrcCRgIJoJYFJ5lzqmWE,8015
11
- statemachine/graph.py,sha256=KtwB1CYckaLjTgQD9tEeuaEzJje9q3fPVpBViW5TgSk,487
12
- statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
13
- statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=MfODCwzML6DzhOpHbno2Pdhsb26AWboR5Ka1JCNvJIA,1685
14
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=KsR9rlnhw17EZG-IbVx3F0wJAJSK0B-OByGmUmDHDUM,2375
15
- statemachine/mixins.py,sha256=B8WB3EGyZpMWAQg4Nw3kUMCfswgmLCChDpOQdxR28eE,1017
16
- statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
17
- statemachine/registry.py,sha256=RnVBRS3I_6Tm2OMgXMB_ewX7zQaslqEfhXFOhbqIkG4,959
18
- statemachine/signature.py,sha256=9KItptchQlP2VpNmBiIFXLNAWNWY3Bs_ymUd7OOgDD4,7507
19
- statemachine/state.py,sha256=bZki0DyemZa-aVAfollQCmpXxesWONzUmE28eDfqz3o,8315
20
- statemachine/statemachine.py,sha256=U0rLIUhvsCivth1uNb0pt2xWVUnDbzhXF982yJH-bg0,11659
21
- statemachine/states.py,sha256=gdGemJYF9k-cifs9Tk0Pe_-1u1Lanf3l08o0t8wAMgg,4203
22
- statemachine/transition.py,sha256=ebRhJsjOoLya7oYZVjRAKvjq1EsC9ImpExgHANekVMA,5388
23
- statemachine/transition_list.py,sha256=DatsmMWgK0YK30Nrj-josVvlTgeGapKutzYur9-puF8,5949
24
- statemachine/utils.py,sha256=fV-Hz1gnyl4tAs9nPr2u9I9zfxq62mokPV8mUpIJc1k,458
25
- python_statemachine-2.2.0.dist-info/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
26
- python_statemachine-2.2.0.dist-info/METADATA,sha256=qH4SxonFNJStkKVocZABu-RLiYD54DcON2gNGjBPGGQ,11454
27
- python_statemachine-2.2.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
28
- python_statemachine-2.2.0.dist-info/RECORD,,