Skip to content

Commit

Permalink
Merge pull request writer#471 from FabienArcellier/50-use-middleware-…
Browse files Browse the repository at this point in the history
…to-intercept-event-handlers-for-database-session-management

use middleware to intercept event handlers (global event, database management, ...)
  • Loading branch information
ramedina86 authored Jun 24, 2024
2 parents c7f131c + b7caf3d commit 74eef7e
Show file tree
Hide file tree
Showing 10 changed files with 527 additions and 132 deletions.
49 changes: 47 additions & 2 deletions docs/framework/event-handlers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -232,15 +267,25 @@ 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):
last_source_of_click = context['target']
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
Expand Down
31 changes: 31 additions & 0 deletions src/writer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
State,
WriterState,
base_component_tree,
get_app_process,
initial_state,
new_initial_state,
session_manager,
Expand Down Expand Up @@ -128,3 +129,33 @@ def init_handlers(handler_modules: Union[List[ModuleType], ModuleType]):

for module in handler_modules:
handler_registry.register_module(module)


def middleware():
"""
A "middleware" is a function that works with every event handler before it is processed and also before returning it.
>>> import writer as wf
>>>
>>> @wf.middleware()
>>> def my_middleware(state):
>>> state['processing'] += 1
>>> yield
>>> state['processing'] -= 1
Middleware accepts the same arguments as an event handler.
>>> import writer as wf
>>>
>>> @wf.middleware()
>>> def my_middleware(state, payload, session):
>>> state['processing'] += 1
>>> yield
>>> state['processing'] -= 1
"""
def inner(func):
_app_process = get_app_process()
_app_process.middleware_registry.register(func)


return inner
35 changes: 34 additions & 1 deletion src/writer/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from watchdog.observers.polling import PollingObserver

from writer import VERSION
from writer.core import EventHandlerRegistry, WriterSession
from writer.core import EventHandlerRegistry, MiddlewareRegistry, WriterSession
from writer.core_ui import ingest_bmc_component_tree
from writer.ss_types import (
AppProcessServerRequest,
Expand All @@ -33,6 +33,8 @@
InitSessionRequest,
InitSessionRequestPayload,
InitSessionResponsePayload,
StateContentRequest,
StateContentResponsePayload,
StateEnquiryRequest,
StateEnquiryResponsePayload,
WriterEvent,
Expand Down Expand Up @@ -96,6 +98,7 @@ def __init__(self,
self.is_app_process_server_failed = is_app_process_server_failed
self.logger = logging.getLogger("app")
self.handler_registry = EventHandlerRegistry()
self.middleware_registry = MiddlewareRegistry()


def _load_module(self) -> ModuleType:
Expand Down Expand Up @@ -204,6 +207,19 @@ def _handle_state_enquiry(self, session: WriterSession) -> StateEnquiryResponseP
session.session_state.clear_mail()

return res_payload

def _handle_state_content(self, session: WriterSession) -> StateContentResponsePayload:
serialized_state = {}
try:
serialized_state = session.session_state.user_state.to_raw_state()
except BaseException:
import traceback as tb
session.session_state.add_log_entry("error",
"Serialisation Error",
"An exception was raised during serialisation.",
tb.format_exc())

return StateContentResponsePayload(state=serialized_state)

def _handle_component_update(self, session: WriterSession, payload: ComponentUpdateRequestPayload) -> None:
import writer
Expand Down Expand Up @@ -255,6 +271,13 @@ def _handle_message(self, session_id: str, request: AppProcessServerRequest) ->
payload=self._handle_state_enquiry(session)
)

if type == "stateContent":
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=self._handle_state_content(session)
)

if type == "setUserinfo":
session.userinfo = request.payload
return AppProcessServerResponse(
Expand Down Expand Up @@ -713,6 +736,16 @@ async def handle_state_enquiry(self, session_id: str) -> AppProcessServerRespons
type="stateEnquiry"
))

async def handle_state_content(self, session_id: str) -> AppProcessServerResponse:
"""
This method returns the complete status of the application.
It is only accessible through tests
"""
return await self.dispatch_message(session_id, StateContentRequest(
type="stateContent"
))

def save_code(self, session_id: str, code: str) -> None:
if self.mode != "edit":
raise PermissionError("Cannot save code in non-edit mode.")
Expand Down
Loading

0 comments on commit 74eef7e

Please sign in to comment.