Skip to content

Commit

Permalink
[server] improve continue/stop handling
Browse files Browse the repository at this point in the history
  • Loading branch information
david-lev committed Jun 2, 2024
1 parent df6523a commit 3d65c76
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 22 deletions.
2 changes: 1 addition & 1 deletion docs/source/content/examples/sign_up_flow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,7 @@ Sending the Flow
To send the flow we need to initialize the :class:`~pywa.client.WhatsApp` client with some specific parameters:

.. code-block:: python
:title: wa.py
:caption: main.py
:linenos:
import fastapi
Expand Down
80 changes: 77 additions & 3 deletions docs/source/content/handlers/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ See `Here <https://developers.facebook.com/docs/development/create-an-app/app-da

.. code-block:: python
:caption: main.py
:emphasize-lines: 10, 11, 12, 13, 14
:linenos:
:emphasize-lines: 4, 9, 10, 11, 12, 13
from fastapi import FastAPI
from pywa import WhatsApp
Expand Down Expand Up @@ -102,7 +103,8 @@ So, start the server:

.. code-block:: python
:caption: main.py
:emphasize-lines: 10, 11
:linenos:
:emphasize-lines: 4, 9, 10
from fastapi import FastAPI
from pywa import WhatsApp
Expand Down Expand Up @@ -189,7 +191,7 @@ A callback function is a function that takes two (positional) arguments:
- The WhatsApp client object (:class:`~pywa.client.WhatsApp`)
- The update object (:class:`~pywa.types.Message`, :class:`~pywa.types.CallbackButton`, etc.)

Here is an example of a callback function that prints messages
Here is an example of a callback functions

.. code-block:: python
:emphasize-lines: 1, 4
Expand All @@ -208,6 +210,9 @@ Using decorators
The easiest way to register a callback function is to use the ``on_message`` and the other ``on_...`` decorators:

.. code-block:: python
:caption: main.py
:linenos:
:emphasize-lines: 8, 13
from pywa import WhatsApp
from pywa.types import Message, CallbackButton
Expand Down Expand Up @@ -241,6 +246,7 @@ main code, or when you want to dynamically register handlers programmatically.

.. code-block:: python
:caption: my_handlers.py
:linenos:
from pywa import WhatsApp
from pywa.types import Message, CallbackButton
Expand All @@ -254,6 +260,8 @@ main code, or when you want to dynamically register handlers programmatically.
.. code-block:: python
:caption: main.py
:linenos:
:emphasize-lines: 9, 10, 11, 12
from pywa import WhatsApp
from pywa.handlers import MessageHandler, CallbackButtonHandler
Expand All @@ -279,6 +287,72 @@ main code, or when you want to dynamically register handlers programmatically.
See how to filter updates in `Filters <filters/overview.html>`_.


Stop or continue handling updates
_________________________________

When a handler is called, when it finishes, in default, the next handler will be called.

.. code-block:: python
:caption: main.py
:linenos:
from pywa import WhatsApp
from pywa.types import Message
wa = WhatsApp(...)
@wa.on_message()
def handle_message(client: WhatsApp, message: Message):
print(message)
# The next handler will be called
@wa.on_message()
def handle_message2(client: WhatsApp, message: Message):
print(message)
# The next handler will be called
...
You can change this behavior by setting the ``continue_handling`` to ``False`` when initializing :class:`~pywa.client.WhatsApp`.

.. code-block:: python
:caption: main.py
:linenos:
:emphasize-lines: 1
wa = WhatsApp(..., continue_handling=False)
@wa.on_message()
def handle_message(client: WhatsApp, message: Message):
print(message)
# The next handler will NOT be called
...
You can also change this behavior inside the callback function by calling the :meth:`~pywa.types.base_update.BaseUpdate.stop_handling`
or :meth:`~pywa.types.base_update.BaseUpdate.continue_handling` methods on the update object.

.. code-block:: python
:caption: main.py
:linenos:
:emphasize-lines: 10, 12
from pywa import WhatsApp, filters
from pywa.types import Message
wa = WhatsApp(...)
@wa.on_message(filters.text)
def handle_message(client: WhatsApp, message: Message):
print(message)
if message.text == 'stop':
message.stop_handling() # The next handler will NOT be called
else:
message.continue_handling() # The next handler will be called
...
Available handlers
__________________

Expand Down
6 changes: 6 additions & 0 deletions docs/source/content/types/others.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ Others
.. currentmodule:: pywa.utils

.. autoclass:: Version()

.. currentmodule:: pywa.types.base_update

.. autoclass:: StopHandling()

.. autoclass:: ContinueHandling()
13 changes: 8 additions & 5 deletions pywa/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,13 @@ def __init__(
self.callback = callback
self.filters = filters

async def handle(self, wa: WhatsApp, data: Any):
async def handle(self, wa: WhatsApp, data: Any) -> bool:
for f in self.filters:
if inspect.iscoroutinefunction(f):
if not await f(wa, data):
return
elif not f(wa, data):
return
return False
elif not await wa._loop.run_in_executor(wa._executor, f, wa, data):
return False

if inspect.iscoroutinefunction(self.callback):
await self.callback(wa, data)
Expand All @@ -259,6 +259,7 @@ async def handle(self, wa: WhatsApp, data: Any):
wa,
data,
)
return True

@staticmethod
@functools.cache
Expand Down Expand Up @@ -339,7 +340,7 @@ def __init__(
self.factory_before_filters = factory_before_filters
super().__init__(callback, *filters)

async def handle(self, wa: WhatsApp, data: Any):
async def handle(self, wa: WhatsApp, data: Any) -> bool:
update = await _get_factored_update(self, wa, data, self._data_field)
if update is not None:
if inspect.iscoroutinefunction(self.callback):
Expand All @@ -351,6 +352,8 @@ async def handle(self, wa: WhatsApp, data: Any):
wa,
update,
)
return True
return False

def __str__(self) -> str:
return f"{self.__class__.__name__}(callback={self.callback!r}, filters={self.filters!r}, factory={self.factory!r})"
Expand Down
28 changes: 15 additions & 13 deletions pywa/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,11 @@ async def my_webhook_handler(req: web.Request) -> web.Response:
return "ok", 200
self._updates_ids_in_process.add(update_id)
await self._call_handlers(update)
if self._skip_duplicate_updates and update_id is not None:
if update_id is not None:
if self._skip_duplicate_updates:
try:
self._updates_ids_in_process.remove(update_id)
except KeyError:
pass
return "ok", 200

def _register_routes(self: "WhatsApp") -> None:
Expand Down Expand Up @@ -355,22 +357,21 @@ async def _call_callbacks(
constructed_update: BaseUpdate | dict,
) -> None:
"""Call the handler type callbacks for the given update."""
handled = False
for handler in self._handlers[handler_type]:
try:
await handler.handle(self, constructed_update)
if not self._continue_handling:
break
handled = await handler.handle(self, constructed_update)
except StopHandling:
break
except ContinueHandling:
continue
except Exception as e:
if isinstance(e, StopHandling):
break
except Exception:
_logger.exception(
"An error occurred while %s was handling an update",
handler.callback.__name__,
)
if handled and not self._continue_handling:
break

def _get_handler(self: "WhatsApp", update: dict) -> type[Handler] | None:
"""Get the handler for the given update."""
Expand Down Expand Up @@ -607,11 +608,12 @@ async def flow_request_handler(payload: dict) -> tuple[str, int]:

return "Failed to construct FlowRequest", 500
try:
response = (
await callback(self, request)
if asyncio.iscoroutinefunction(callback)
else callback(self, request)
)
if asyncio.iscoroutinefunction(callback):
response = await callback(self, request)
else:
response = await self._loop.run_in_executor(
self._executor, callback, self, request
)
if isinstance(response, FlowResponseError):
raise response
except FlowTokenNoLongerValid as e:
Expand Down

0 comments on commit 3d65c76

Please sign in to comment.