Package: tomata
Welcome totomatareadme!This is a generic state automata-ish module allowing you to build an typed event-driven stateful flow with minimal setup.The basic flow is:You define your own emitter class, inheriting fromtomata.Emitter. There you defineget_stateandset_statemethods to provide the state storage.You create an instance of emitterYou register handlers for the state changes and events withemitter.on(state_key, handler_type = "event")In your main loop, you callemitter.emit(event, identity)with arbitrary event and identity values (identity is passed toget_state,set_stateand handler, event is passed to handler)If you want to dive into examples, go toexamplesfolder.Conceptstomata works around several concepts (initalicin text):EventEventis an arbitrary value to process. It is passed to all thehandlersStateStateis some key to denote the current state.Handlersare called depending on which state is currently activeIdentityIdentityis a key that is used to differentiate between states of different entites. An identity could be, for example, a user id or a string hash.HandlerHandleris a function called on any event. There are three types of handlers:enter,eventandleave.enterhandlers are called whenstateswitchestothe one tied to handlereventhandlers are called whenemitteremits an event. Handlers of this type can change thestateby returning the nextstatekeyleavehandlers are called whenstateswitchesfromthe one tied to handlerEmitterEmitteris the main object of the module. It consumeseventsand callshandlersas a result.Handlerscan request astatechange,emitteralso handles that.To work with events, you need your ownEmitterclass (and an instance). It manages calling your handlers and changing the state.A minimalEmitterclass has to defineget_stateandset_state:fromtomataimportEmitterclassMyEmitter(Emitter):defset_state(self,identity,state):...defget_state(self,identity):...Those methods should handle how to store the state (for example, get_state could retrieve the state from database and set_state could write it into the database).You can also define__init__for your emitter class:...classMyEmitter(Emitter):def__init__(self):default_state="start"super().__init__(default_state)......If you love type hints, we've got you covered:fromtomataimportEmitterfromtypingimportLiteralStateKey=Literal["start","age","name","finish"]Event=strIdentity=intclassMyEmitter(Emitter[Event,StateKey,Identity]):def__init__(self):super().__init__("start")defget_state(self,identity:Identity)->StateKey:...defset_state(self,identity:Identity,state:StateKey)->None:......or, if you are on Python 3.12:fromtomataimportEmitterfromtypingimportLiteral# those types are arbitrary, you can actually use anything as your event / state / identitytypeEvent=strtypeStateKey=Literal["start","age","name","finish"]typeIdentity=intclassMyEmitter(Emitter[Event,StateKey,Identity]):def__init__(self):super().__init__("start")defget_state(self,identity:Identity)->StateKey:...defset_state(self,identity:Identity,state:StateKey)->None:...Defining handlersLet's say, you want to run some code when statecake_shootingis active and some event comes by:em=MyEmitter()...@em.on("cake_shooting")defcake_shooting(event:Event,identity:Identity,state:StateKey):# event and identity are provided through emit, state is the current state.ifnotevent:return"no_cake"# new statecake=catch_cake(event)eat_cake(cake)Now, if you want to run some code when state was switched tocake_shooting, useenterhandler:em=MyEmitter()...@em.on.enter("cake_shooting")defcake_shooting(event:Event,identity:Identity,state:StateKey):# event and identity are provided through emit, state is the current state....Same goes for when state was switched from state, just use.leavehandlerEmitting eventsAfter you've set up your emitter and handlers, it's time to emit some events:emitter=MyEmitter()fromjsonimportloadfromtypingimportTypedDictclassEventData(TypedDict):data:Eventidentity:Identitywithopen("events.json")asfile:events:list[EventData]=load(file)foreventinevents:emitter.emit(event["data"],event["identity"])Default handlerYou can set a default (fallback) handler to handle events when no other handler is found:@em.on.defaultdefdefault_handler(event,identity,state):...At the moment, you can't define a defaultenterandleavehandlers, because it doesn't really seem useful (but feel free to open an issue if you find a good use-case)AsyncTo use async, replace Emitter with AsyncEmitter:fromtomataimportAsyncEmitterfromtypingimportLiteraltypeStateKey=Literal["start","age","name","finish"]typeEvent=strtypeIdentity=intclassMyEmitter(AsyncEmitter[Event,StateKey,Identity]):def__init__(self):super().__init__("start")asyncdefget_state(self,identity:Identity)->StateKey:...asyncdefset_state(self,identity:Identity,state:StateKey)->None:...em=MyEmitter()# AsyncEmitter can call both async and sync handlers@em.on("funny_pineapple")asyncdeffunny_pineapple(event:Event,identity:Identity,state:StateKey):...@em.on("sad_pineapple")defsad_pineapple(event:Event,identity:Identity,state:StateKey):...Advanced behaviourLet's say, you want to redefine handler storage.
You can easily do that by defining your ownget_handlerandset_handlermethods.Let's make an Emitter where state would be a dictionary, and the handler would be called on state["type"]fromtomataimportEmitterfromtomata.baseimportmake_handlers_dictEvent=dict[str,str]State=dict[str,str]Identity=intclassAdvancedEmitter(Emitter[Event,State,Identity])handlers:dict[HandlerType,SyncHandlerStore[str,Event,Identity]]defget_state(self,identity:Identity)->State:...defset_state(self,identity:Identity,state:State)->None:...defget_handler(self,state:SK,handler_type:HandlerType)->SyncHandler[Ev,Id,SK]|None:store=self.handlers[handler_type]key=state.get("type",self.default_state)returnstore.get(key)defset_handler(self,state:SK,handler_type:HandlerType,handler:SyncHandler[Ev,Id,SK],):key=state.get("type",self.default_state)store=self.handlers[handler_type]store[key]=handlerem=AdvancedEmitter({"type":"start"})@em.on({"type":"start"})defstart_event(event:Event,identity:Identity,state:State):new_state={**state,"type":"ongoing"}new_state["count"]=new_state.get("count",0)returnnew_stateTo redefine setting fallback handler logic, define your ownset_default(handler)andget_default()methods.AsyncEmitter methodsTo extend AsyncEmitter, you have to remember two things:Some methods ([get|set]_state,get_handler,get_default) are async. This means you have to define them withasync defeven if you don't plan to use await in them.emitis obviously async, but if you don't need an asyncemit, take a look at synchronousEmitterInset_handlerandset_default, you have to async-ify the handlers. To do that, usetomata.async_emitter.make_asyncfunction, which takes any handler and makes it asyncOtherwise, the extension process should be just as withEmitter.