1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
Events
======
Litestar supports a simple implementation of the event emitter / listener pattern:
.. code-block:: python
from dataclasses import dataclass
from litestar import Request, post
from litestar.events import listener
from litestar import Litestar
from db import user_repository
from utils.email import send_welcome_mail
@listener("user_created")
async def send_welcome_email_handler(email: str) -> None:
# do something here to send an email
await send_welcome_mail(email)
@dataclass
class CreateUserDTO:
first_name: str
last_name: str
email: str
@post("/users")
async def create_user_handler(data: UserDTO, request: Request) -> None:
# do something here to create a new user
# e.g. insert the user into a database
await user_repository.insert(data)
# assuming we have now inserted a user, we want to send a welcome email.
# To do this in a none-blocking fashion, we will emit an event to a listener, which will send the email,
# using a different async block than the one where we are returning a response.
request.app.emit("user_created", email=data.email)
app = Litestar(
route_handlers=[create_user_handler], listeners=[send_welcome_email_handler]
)
The above example illustrates the power of this pattern - it allows us to perform async operations without blocking,
and without slowing down the response cycle.
Listening to Multiple Events
++++++++++++++++++++++++++++
Event listeners can listen to multiple events:
.. code-block:: python
from litestar.events import listener
@listener("user_created", "password_changed")
async def send_email_handler(email: str, message: str) -> None:
# do something here to send an email
await send_email(email, message)
Using Multiple Listeners
++++++++++++++++++++++++
You can also listen to the same events using multiple listeners:
.. code-block:: python
from uuid import UUID
from dataclasses import dataclass
from litestar import Request, post
from litestar.events import listener
from db import user_repository
from utils.client import client
from utils.email import send_farewell_email
@listener("user_deleted")
async def send_farewell_email_handler(email: str, **kwargs) -> None:
# do something here to send an email
await send_farewell_email(email)
@listener("user_deleted")
async def notify_customer_support(reason: str, **kwargs) -> None:
# do something here to send an email
await client.post("some-url", reason)
@dataclass
class DeleteUserDTO:
email: str
reason: str
@post("/users")
async def delete_user_handler(data: UserDTO, request: Request) -> None:
await user_repository.delete({"email": email})
request.app.emit("user_deleted", email=data.email, reason="deleted")
In the above example we are performing two side effect for the same event, one sends the user an email, and the other
sending an HTTP request to a service management system to create an issue.
Passing Arguments to Listeners
++++++++++++++++++++++++++++++
The method :meth:`emit <litestar.events.BaseEventEmitterBackend.emit>` has the following signature:
.. code-block:: python
def emit(self, event_id: str, *args: Any, **kwargs: Any) -> None: ...
This means that it expects a string for ``event_id`` following by any number of positional and keyword arguments. While
this is highly flexible, it also means you need to ensure the listeners for a given event can handle all the expected args
and kwargs.
For example, the following would raise an exception in python:
.. code-block:: python
@listener("user_deleted")
async def send_farewell_email_handler(email: str) -> None:
await send_farewell_email(email)
@listener("user_deleted")
async def notify_customer_support(reason: str) -> None:
# do something here to send an email
await client.post("some-url", reason)
@dataclass
class DeleteUserDTO:
email: str
reason: str
@post("/users")
async def delete_user_handler(data: UserDTO, request: Request) -> None:
await user_repository.delete({"email": email})
request.app.emit("user_deleted", email=data.email, reason="deleted")
The reason for this is that both listeners will receive two kwargs - ``email`` and ``reason``. To avoid this, the previous example
had ``**kwargs`` in both:
.. code-block:: python
@listener("user_deleted")
async def send_farewell_email_handler(email: str, **kwargs) -> None:
await send_farewell_email(email)
@listener("user_deleted")
async def notify_customer_support(reason: str, **kwargs) -> None:
await client.post("some-url", reason)
Creating Event Emitters
-----------------------
An "event emitter" is a class that inherits from
:class:`BaseEventEmitterBackend <litestar.events.BaseEventEmitterBackend>`, which
itself inherits from :obj:`contextlib.AbstractAsyncContextManager`.
- :meth:`emit <litestar.events.BaseEventEmitterBackend.emit>`: This is the method that performs the actual emitting
logic.
Additionally, the abstract ``__aenter__`` and ``__aexit__`` methods from
:obj:`contextlib.AbstractAsyncContextManager` must be implemented, allowing the
emitter to be used as an async context manager.
By default Litestar uses the
:class:`SimpleEventEmitter <litestar.events.SimpleEventEmitter>`, which offers an
in-memory async queue.
This solution works well if the system does not need to rely on complex behaviour, such as a retry
mechanism, persistence, or scheduling/cron. For these more complex use cases, users should implement their own backend
using either a DB/Key store that supports events (Redis, Postgres, etc.), or a message broker, job queue, or task queue
technology.
|