Skip to content

Commit

Permalink
Merge branch 'writer:dev' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
anant-writer authored Jun 25, 2024
2 parents ced3190 + 74eef7e commit b92e804
Show file tree
Hide file tree
Showing 12 changed files with 541 additions and 140 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
17 changes: 9 additions & 8 deletions src/ui/src/builder/BuilderFieldsPadding.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
@select="handleInputSelect"
/>
<div v-if="subMode == SubMode.all_sides" class="row">
<i class="material-symbols-outlined">padding</i>
<i>All</i>
<input
ref="fixedEl"
type="text"
Expand All @@ -53,7 +53,7 @@
</div>
<div v-if="subMode == SubMode.xy_sides">
<div class="row">
<i class="material-symbols-outlined">padding</i>
<i>X</i>
<input
ref="fixedEl"
type="text"
Expand All @@ -63,7 +63,7 @@
<div>px</div>
</div>
<div class="row">
<i class="material-symbols-outlined">padding</i>
<i>Y</i>
<input
type="text"
:value="valuePadding[2]"
Expand All @@ -74,7 +74,7 @@
</div>
<div v-if="subMode == SubMode.per_side">
<div class="row">
<i class="material-symbols-outlined">padding</i>
<i>Left</i>
<input
ref="fixedEl"
type="text"
Expand All @@ -84,7 +84,7 @@
<div>px</div>
</div>
<div class="row">
<i class="material-symbols-outlined">padding</i>
<i>Right</i>
<input
type="text"
:value="valuePadding[1]"
Expand All @@ -93,7 +93,7 @@
<div>px</div>
</div>
<div class="row">
<i class="material-symbols-outlined">padding</i>
<i>Top</i>
<input
type="text"
:value="valuePadding[2]"
Expand All @@ -102,7 +102,7 @@
<div>px</div>
</div>
<div class="row">
<i class="material-symbols-outlined">padding</i>
<i>Bottom</i>
<input
type="text"
:value="valuePadding[3]"
Expand Down Expand Up @@ -412,10 +412,11 @@ onBeforeUnmount(() => {
flex-direction: row;
gap: 8px;
padding: 8px;
align-items: center;
align-items: baseline;
}
.row input {
width: calc(100% - 32px) !important;
text-align: right;
}
</style>
5 changes: 5 additions & 0 deletions src/ui/src/builder/BuilderSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ const select = (event) => {
display: block;
padding: 8px;
font-weight: 400;
font-size: 0.75rem;
color: #000000e6;
cursor: pointer;
transition: all 0.2s;
Expand Down Expand Up @@ -182,4 +183,8 @@ const select = (event) => {
flex-direction: row;
gap: 8px;
}
.selectContent {
font-size: 0.75rem;
}
</style>
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 b92e804

Please sign in to comment.