Events¶
FIXME¶
I added a new dispatch_async. Basically events should either be sync or async. For example, all bootstrapping events are sync since bootstrap is sync. But all events coming from any async source like a route or a database query must be async as well. So the ORM for example will dispatch all events as dispatch_async therefore those events are async and all handlers should also be async.
Introduction¶
Uvicore provides an observer (pub/sub) implementation allowing you to listen (subscribe) to events that occur in the framework and in your own packages.
Events are a great way to decouple various aspects of your project. A single event can have many listeners that need not depend on each other. For example, each time a wiki post is created you may wish to send an email or slack notification to all watchers of the post.
All events must be pre-registered with uvicore. This allows the framework to
know all possible events for all packages. Running ./uvicore events list
will
show you this events database.
Events are dispatched throughout the framework and other packages. When an event is dispatched all defined listeners will be called in order they were added. Listeners define the event(s) to watch and the event handlers (callbacks) to fire when then event is dispatched. Handlers can be methods, classes or even bulk listening/handling subscriptions.
Defining Your Own Events¶
The first step in creating your own events is to pick an event name, then
register that event with the framework. The best place to register your own
events is in your package Service Provider register()
method.
Register Events
# services/wiki.py
from uvicore.support.provider import ServiceProvider
class Wiki(ServiceProvider):
def register(self):
# Register events used in this package
self.events({
'mreschke.wiki.events.post.Created': {
'description': 'Wiki post has been created',
'type': 'class',
# ...
# There is no set definition of what should be here. This is
# your own metadata for an event. Could define log output
# text, log icons or sections, priority, severity or anything
# you want. This metadata is passed along to each handler.
},
'mreschke.wiki.events.post.Deleted': {
'description': 'Wiki post has been deleted',
'type': 'class',
},
})
# ...
The second step is OPTIONAL. You must decide if you want to create a matching event class that acts as a payload container and parameter contract. Or simply treat the event as a string based event with Dictionary payload parameters.
The benifits of an event class are that you can force the payload requirements
using the class __init__()
constructor. An event class performs NO work. It
is simply a data container for the payload itself. Event classes are typically
stored in the events
directory of your package. If using a simple string to
define an event, it is best practice to name the string as if it were going to
be an actual event class someday. For example
mreschke.wiki.events.post.Created
. If at some point you want to create the
actual class, all existing listeners that use that string will not need to be
changed as the system looks for a class of the same name to instantiate.
Event Payload Class
# events/post.py
from uvicore.events import Event
from mreschke.wiki.models import Post
class Created(Event):
def __init__(self, post: Post):
self.post = post
That's it. Seems simple. An event class contains no logic. It is simply a container with a constructor that forces a specific payload. This helps constrain the dispatch code and also provides Code IDE intellisense when newing up the event to dispatch. This class is completely optional. The event can still be dispatched without it.
Dispatching Events¶
You can fire off (dispatch) a registered event in a few different ways. Where you fire off the event is up to your own code. With the Wiki events example above the proper place may be in your controller, model or job that Creates and Deletes wiki posts. Wherever the location, fireing off an event is simple.
As a String
The framework will use this string to check if a matching event class exists.
If the class exists it will import it and new it up using the dictionary as
the __init__()
constructor parameters. If the class does not exist, this
event will still fire as usual using the dictionary as an unconstrained payload.
from uvicore import events
events.dispatch('mreschke.wiki.events.post.Created', {'post': post})
As a Class Instance
You can new up the event class yourself. The benefits of this over the string approach is IDE auto-completion. Your IDE will show you the exact parameters required to new up the class. The payload is now constrained to a contract.
When newing up the event class yourself you have two options of how to dispatch it.
from uvicore import events
from mreschke.wiki.events import Created
# Using the events.dispatch() method to pass in the class instance
events.dispatch(Created(post))
# Or by using the event classes build-in .dispatch() method
Created(post).dispatch()
However you dispatch your events, all listeners are immediately fired in order they were defined.
Listening to Events¶
Registering and then dispatching events do nothing if there is no one listening. Listeners define callbacks that are executed when an event is dispatched.
The best place to register your event listeners is in your package Service
Provider register()
method which has access to self.listen
and
self.subscribe
event helper methods.
Listen to a single event
# From service provider register() method
# Use a local method (function) as the callback
self.listen('mreschke.wiki.post.Created', self.NotifyUsers)
# Use a listener class as the callback
self.listen('mreschke.wiki.post.Created', 'mreschke.wiki.listeners.NotifyUsers')
Info
Notice the mreschke.wiki.listeners.NotifyUsers
class is defined as a
string in dot notation. The event system will automatically instantiate
and call the class handle()
method during dispatch.
Listen to multiple events
# From service provider register() method
# Use a local method (function) as the callback
self.listen([
'mreschke.wiki.post.Created',
'mreschke.wiki.post.Deleted',
], self.NotifyUsers)
# Use a listener class as the callback
self.listen([
'mreschke.wiki.post.Created',
'mreschke.wiki.post.Deleted',
], 'mreschke.wiki.listeners.NotifyUsers')
Listen to wildcard events
# From service provider register() method
# Use a local method (function) as the callback
self.listen('uvicore.foundation.events.*', self.NotifyUsers)
# Use a listener class as the callback
self.listen('uvicore.foundation.events.*', 'mreschke.wiki.listeners.NotifyUsers')
# The * wildcard also works in the middle of an event name
self.listen('mreschke.wiki.models.*.Deleted', self.LogDeletions)
Registering a subscriber
A subscription is an all-in-one class which listens to one or more events and also contains the handlers for each event. Notice we are not defining the event to listen to here. We simply define the subscription class. See Handling Events for what these classes look like.
# From service provider register() method
self.subscribe('mreschke.wiki.listeners.HttpEventSubscription')
Listeners outside a Service Provider¶
You can also listen and subscribe to events outside of a service provider by
using the uvicore.events
instance.
from uvicore import events
events.listen('mreschke.wiki.post.Created', self.my_handler)
events.subscribe('mreschke.wiki.listeners.HttpEventSubscription')
Handling Events¶
Handlers are callbacks that are dispatched when an event fires. Handlers are
defined using listen()
or subscribe()
methods as noted in
Listening To Events.
Handlers can be basic python methods (functions) or dedicated handler classes.
All handlers receive an event: Dict
and payload: Any
. The event
dictionary is the metadata for an event that was defined by the developer
during the event registration. If the event listener is a class, the payload
will be an instance of that class with properties of all constructor parameters.
If the event listener is just a string, the payload is a namedtuple
of
parameters.
Method Handler
def my_handler(event: Dict, payload: Any) -> None:
# Do work when this event is dispatched.
Class Handler
from typing import Dict, Any
from uvicore.events.handler import Handler
class NotifyUser(Handler):
def handle(self, event: Dict, payload: Any):
# Instance variable self.app is also available to you
# Do work when this event is dispatched.
Subscription Handlers
Subscriptions are a great way to listen and handle multiple events from a single file.
from typing import Dict, Any
from uvicore.contracts import Dispatcher
class AppEventSubscription:
def app_registered(self, event: Dict, payload: Any):
# Do something when then the framework is done registering all providers
def app_booted(self, event: Dict, payload: Any):
# Do something when then the framework is done booting all providers
def post_created(self, event: Dict, payload: Any):
# Do something when a wiki post is created
def subscribe(self, events: Dispatcher):
# A subscription is an all in one class that can both listen AND handle
# one or more events in a single place.
events.listen('uvicore.foundation.events.app.Registered', self.app_registered)
events.listen('uvicore.foundation.events.app.Booted', self.app_booted)
events.listen('mreschke.wiki.post.Created', self.post_created)