Package: python-statemachine
Python StateMachinePythonfinite-state machinesmade easy.Free software: MIT licenseDocumentation:https://python-statemachine.readthedocs.io.Welcome to python-statemachine, an intuitive and powerful state machine framework designed for a
great developer experience.🚀 With StateMachine, you can easily create complex, dynamic systems with clean, readable code.💡 Our framework makes it easy to understand and reason about the different states, events and
transitions in your system, so you can focus on building great products.🔒 python-statemachine also provides robust error handling and ensures that your system stays
in a valid state at all times.A few reasons why you may consider using it:📈 python-statemachine is designed to help you build scalable,
maintainable systems that can handle any complexity.💪 You can easily create and manage multiple state machines within a single application.🚫 Prevents common mistakes and ensures that your system stays in a valid state at all times.Getting startedTo install Python State Machine, run this command in your terminal:pip install python-statemachineTo generate diagrams from your machines, you'll also needpydotandGraphviz. You can
install this library already withpydotdependency using theextrasinstall option. See
our docs for more details.pip install python-statemachine[diagrams]Define your state machine:>>>fromstatemachineimportStateMachine,State>>>classTrafficLightMachine(StateMachine):..."A traffic light machine"...green=State(initial=True)...yellow=State()...red=State()......cycle=(...green.to(yellow)...|yellow.to(red)...|red.to(green)...)......defbefore_cycle(self,event:str,source:State,target:State,message:str=""):...message=". "+messageifmessageelse""...returnf"Running{event}from{source.id}to{target.id}{message}"......defon_enter_red(self):...print("Don't move.")......defon_exit_red(self):...print("Go ahead!")You can now create an instance:>>>sm=TrafficLightMachine()This state machine can be represented graphically as follows:>>>img_path="docs/images/readme_trafficlightmachine.png">>>sm._graph().write_png(img_path)Where on theTrafficLightMachine, we've definedgreen,yellow, andredas states, and
one event calledcycle, which is bound to the transitions fromgreentoyellow,yellowtored,
andredtogreen. We also have defined three callbacks by name convention,before_cycle,on_enter_red, andon_exit_red.Then start sending events to your new state machine:>>>sm.send("cycle")'Running cycle from green to yellow'That's it. This is all an external object needs to know about your state machine: How to send events.
Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.But if your use case needs, you can inspect state machine properties, like the current state:>>>sm.current_state.id'yellow'Or get a complete state representation for debugging purposes:>>>sm.current_stateState('Yellow',id='yellow',value='yellow',initial=False,final=False)TheStateinstance can also be checked by equality:>>>sm.current_state==TrafficLightMachine.yellowTrue>>>sm.current_state==sm.yellowTrueOr you can check if a state is active at any time:>>>sm.green.is_activeFalse>>>sm.yellow.is_activeTrue>>>sm.red.is_activeFalseEasily iterate over all states:>>>[s.idforsinsm.states]['green','red','yellow']Or over events:>>>[t.namefortinsm.events]['cycle']Call an event by its name:>>>sm.cycle()Don't move.'Running cycle from yellow to red'Or send an event with the event name:>>>sm.send('cycle')Goahead!'Running cycle from red to green'>>>sm.green.is_activeTrueYou can pass arbitrary positional or keyword arguments to the event, and
they will be propagated to all actions and callbacks using something similar to dependency injection. In other words, the library will only inject the parameters declared on the
callback method.Note howbefore_cyclewas declared:defbefore_cycle(self,event:str,source:State,target:State,message:str=""):message=". "+messageifmessageelse""returnf"Running{event}from{source.id}to{target.id}{message}"The paramsevent,source,target(and others) are available built-in to be used on any action.
The parammessageis user-defined, in our example we made it default empty so we can callcyclewith
or without amessageparameter.If we pass amessageparameter, it will be used on thebefore_cycleaction:>>>sm.send("cycle",message="Please, now slowdown.")'Running cycle from green to yellow. Please, now slowdown.'By default, events with transitions that cannot run from the current state or unknown events
raise aTransitionNotAllowedexception:>>>sm.send("go")Traceback(mostrecentcalllast):statemachine.exceptions.TransitionNotAllowed:Can't go when in Yellow.Keeping the same state as expected:>>>sm.yellow.is_activeTrueA human-readable name is automatically derived from theState.id, which is used on the messages
and in diagrams:>>>sm.current_state.name'Yellow'A more useful exampleA simple didactic state machine for controlling anOrder:>>>classOrderControl(StateMachine):...waiting_for_payment=State(initial=True)...processing=State()...shipping=State()...completed=State(final=True)......add_to_order=waiting_for_payment.to(waiting_for_payment)...receive_payment=(...waiting_for_payment.to(processing,cond="payments_enough")...|waiting_for_payment.to(waiting_for_payment,unless="payments_enough")...)...process_order=processing.to(shipping,cond="payment_received")...ship_order=shipping.to(completed)......def__init__(self):...self.order_total=0...self.payments=[]...self.payment_received=False...super(OrderControl,self).__init__()......defpayments_enough(self,amount):...returnsum(self.payments)+amount>=self.order_total......defbefore_add_to_order(self,amount):...self.order_total+=amount...returnself.order_total......defbefore_receive_payment(self,amount):...self.payments.append(amount)...returnself.payments......defafter_receive_payment(self):...self.payment_received=True......defon_enter_waiting_for_payment(self):...self.payment_received=FalseYou can use this machine as follows.>>>control=OrderControl()>>>control.add_to_order(3)3>>>control.add_to_order(7)10>>>control.receive_payment(4)[4]>>>control.current_state.id'waiting_for_payment'>>>control.current_state.name'Waiting for payment'>>>control.process_order()Traceback(mostrecentcalllast):...statemachine.exceptions.TransitionNotAllowed:Can't process_order when in Waiting for payment.>>>control.receive_payment(6)[4,6]>>>control.current_state.id'processing'>>>control.process_order()>>>control.ship_order()>>>control.payment_receivedTrue>>>control.order_total10>>>control.payments[4,6]>>>control.completed.is_activeTrueThere's a lot more to cover, please take a look at our docs:https://python-statemachine.readthedocs.io.Contributing to the projectStar this projectOpen an IssueForkIf you found this project helpful, please consider giving it a star on GitHub.Contribute code: If you would like to contribute code to this project, please submit a pull
request. For more information on how to contribute, please see ourcontributing.mdfile.Report bugs: If you find any bugs in this project, please report them by opening an issue
on our GitHub issue tracker.Suggest features: If you have a great idea for a new feature, please let us know by opening
an issue on our GitHub issue tracker.Documentation: Help improve this project's documentation by submitting pull requests.Promote the project: Help spread the word about this project by sharing it on social media,
writing a blog post, or giving a talk about it. Tag me on Twitter@fgmacedoso I can share it too!