From b7caf3d0e659f3832bd577db3aaf9c21d170b8eb Mon Sep 17 00:00:00 2001 From: Fabien Arcellier Date: Mon, 24 Jun 2024 16:34:11 +0200 Subject: [PATCH] use middleware to intercept event handlers (for database session management, ...) * feat: use pattern to handle exceptions * feat: allow middleware without yield * docs: describe the implementation of middlewares --- docs/framework/event-handlers.mdx | 49 +++++++++++++++++++++++++++++-- src/writer/app_runner.py | 4 +-- src/writer/core.py | 6 ++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/docs/framework/event-handlers.mdx b/docs/framework/event-handlers.mdx index 8b902aabb..3000f11d7 100644 --- a/docs/framework/event-handlers.mdx +++ b/docs/framework/event-handlers.mdx @@ -172,6 +172,41 @@ def evaluate(state, payload): Take into account that globals apply to all users. If you need to store data that's only relevant to a particular user, use application state. +## Middlewares + +Middlewares are functions that run before and after every event handler. +They can be used to perform tasks such as logging, error handling, session management, or modifying the state. + +```py +import writer as wf + +@wf.middleware() +def middleware_before(state, payload, context): + print("Middleware before event handler") + state['running'] += 1 + yield + print("Middleware after event handler") + state['running'] -= 1 +``` + +A middleware receives the same parameters as an event handler. + +A middleware can be used to handle exceptions that happens in event handlers. + +```py +import writer as wf + +@wf.middleware() +def middleware_before(state): + try: + yield + except Exception as e: + state['error_counter'] += 1 + state['last_error'] = str() + finally: + pass +``` + ## Standard output The standard output of an app is captured and shown in the code editor's log. You can use the standard `print` function to output results. @@ -232,8 +267,9 @@ You can use any awaitable object within an async event handler. This includes th ## Context -The `context` argument provides additional information about the event. For example, if the event -was triggered by a _Button_, the `context` will include target field that contains the id of the button. +The `context` argument provides additional information about the event. + +The context provide the id of component that trigger the event in `target` field. ```py def handle_click(state, context: dict): @@ -241,6 +277,15 @@ def handle_click(state, context: dict): state["last_source_of_click"] = last_source_of_click ``` +The context provides the event triggered in the `event` field. + +```py +def handle_click(state, context: dict): + event_type = context['event'] + if event_type == 'click': + state["last_event"] = 'Click' +``` + The repeater components have additional fields in the context, such as defined in `keyVariable` and `valueVariable`. ```py diff --git a/src/writer/app_runner.py b/src/writer/app_runner.py index b8ddd86c1..a6cb0fa90 100644 --- a/src/writer/app_runner.py +++ b/src/writer/app_runner.py @@ -208,7 +208,7 @@ def _handle_state_enquiry(self, session: WriterSession) -> StateEnquiryResponseP return res_payload - def _handle_state_full(self, session: WriterSession) -> StateContentResponsePayload: + def _handle_state_content(self, session: WriterSession) -> StateContentResponsePayload: serialized_state = {} try: serialized_state = session.session_state.user_state.to_raw_state() @@ -275,7 +275,7 @@ def _handle_message(self, session_id: str, request: AppProcessServerRequest) -> return AppProcessServerResponse( status="ok", status_message=None, - payload=self._handle_state_full(session) + payload=self._handle_state_content(session) ) if type == "setUserinfo": diff --git a/src/writer/core.py b/src/writer/core.py index 2faa66134..c72a755f7 100644 --- a/src/writer/core.py +++ b/src/writer/core.py @@ -1588,6 +1588,12 @@ def build_writer_func_arguments(func: Callable, writer_args: dict) -> List[Any]: Constructs the list of arguments based on the signature of the function which can be a handler or middleware. + >>> def my_event_handler(state, context): + >>> yield + + >>> args = build_writer_func_arguments(my_event_handler, {'state': {}, 'payload': {}, 'context': {"target": '11'}, 'session': None, 'ui': None}) + >>> [{}, {"target": '11'}] + :param func: the function that will be called :param writer_args: the possible arguments in writer (state, payload, ...) """