File: events.rst

package info (click to toggle)
litestar 2.21.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 12,568 kB
  • sloc: python: 70,588; makefile: 254; javascript: 104; sh: 60
file content (196 lines) | stat: -rw-r--r-- 5,961 bytes parent folder | download
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.