Simple finite state machines.
Perfect for representing workflows.
from definite import FSM # You define all the valid states, as well as what their allowed # transitions are. class Workflow(FSM): allowed_transitions = { "draft": ["awaiting_review", "rejected"], "awaiting_review": ["draft", "reviewed", "rejected"], "reviewed": ["published", "rejected"], "published": None, "rejected": ["draft"], } default_state = "draft" # Right away, you can use the states/transitions as-is to enforce changes. workflow = Workflow() workflow.current_state() # "draft" workflow.transition_to("awaiting_review") workflow.transition_to("reviewed") workflow.is_allowed("published") # True # Invalid/disallowed transitions will throw an exception. workflow.current_state() # "reviewed" # ...which can only go to "published" or "rejected", but... workflow.transition_to("awaiting_review") # Traceback (most recent call last): # ... # workflow.TransitionNotAllowed: "reviewed" cannot transition to "awaiting_review" # Additionally, you can set up extra code to fire on given state changes. class Workflow(FSM): # Same transitions & default state. allowed_transitions = { "draft": ["awaiting_review", "rejected"], "awaiting_review": ["draft", "reviewed", "rejected"], "reviewed": ["published", "rejected"], "published": None, "rejected": ["draft"], } default_state = "draft" # Define a `handle_<state_name>` method on the class. def handle_awaiting_review(self, new_state): spell_check_results = check_spelling(self.obj.content) msg = ( f"{self.obj.title} ready for review. " f"{len(spell_check_results)} spelling errors." ) send_email(to=editor_email, message=msg) def handle_published(self, new_state): self.obj.pub_date = datetime.datetime.utcnow() self.obj.save() # You can also setup code that fires on **ANY** valid transition with the # special `handle_any` method. def handle_any(self, new_state): self.obj.state = new_state self.obj.save() # We can pull in any Python object, like a database-backed model, that we # want to associate with our FSM. from news.models import NewsPost news_post = NewsPost.objects.create( title="Hello world!", content="This iz our frist post!", state="draft", ) # We start mostly the same, but this time pass an `obj` kwarg! workflow = Workflow(obj=news_post) # If you wanted to be explicit, you could also pass along the `initial_state`: workflow = Workflow( obj=news_post, initial_state=news_post.state ) workflow.current_state() # "draft" # But when we trigger this change... workflow.transition_to("awaiting_review") # ...it triggers the spell check & the email we defined above, as well as # hitting the `handle_any` method & updating the `state` field in the DB. news_post.refresh_from_db() news_post.state # "awaiting_review" !
pip install definite
- Python 3.6+
$ pytest .
New BSD