Skip to content

Commit

Permalink
Merge pull request #496 from FabienArcellier/37-trigger-a-calculated-…
Browse files Browse the repository at this point in the history
…property-on-mutation

feat: trigger a calculated property on mutation
  • Loading branch information
ramedina86 authored Aug 19, 2024
2 parents ec80a44 + c2c5822 commit 505f6f0
Show file tree
Hide file tree
Showing 8 changed files with 950 additions and 132 deletions.
30 changes: 30 additions & 0 deletions docs/framework/application-state.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,33 @@ The front-end cannot directly display complex data types such as Pandas datafram
Pandas dataframes are converted to JSON and can be used in _Dataframe_ components.
</Tab>
</Tabs>

## State schema

State schema is a feature that allows you to define the structure of the state.
This is useful for ensuring that the state is always in the expected format.

Schema allows you to use features like

* typing checking with mypy / ruff
* autocomplete in IDEs
* declare dictionaries
* automatically calculate mutations on properties

more into [Advanced > State schema](./state-schema)

```python
import writer as wf

class AppSchema(wf.WriterState):
counter: int

initial_state = wf.init_state({
"counter": 0
}, schema=AppSchema)

# Event handler
# It receives the session state as an argument and mutates it
def increment(state: AppSchema):
state.counter += 1
```
57 changes: 57 additions & 0 deletions docs/framework/event-handlers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,63 @@ def hande_click_cleaner(state):
```
</CodeGroup>

## Mutation event

You can subscribe to mutations on a specific key in the state.
This is useful when you want to trigger a function every time a specific key is mutated.

<CodeGroup>
```python simple subscription
import writer as wf

def _increment_counter(state):
state['my_counter'] += 1

state = wf.init_state({"a": 1, "my_counter": 0})
state.subscribe_mutation('a', _increment_counter)

state['a'] = 2 # trigger _increment_counter mutation
```

```python multiple subscriptions
import writer as wf

def _increment_counter(state):
state['my_counter'] += 1

state = wf.init_state({
'title': 'Hello',
'app': {'title', 'Writer Framework'},
'my_counter': 0}
)

state.subscribe_mutation(['title', 'app.title'], _increment_counter) # subscribe to multiple keys

state['title'] = "Hello Pigeon" # trigger _increment_counter mutation
```

```python trigger event handler
import writer as wf

def _increment_counter(state, context: dict, payload: dict, session: dict, ui: WriterUIManager):
if context['event'] == 'mutation' and context['mutation'] == 'a':
if payload['previous_value'] > payload['new_value']:
state['my_counter'] += 1

state = wf.init_state({"a": 1, "my_counter": 0})
state.subscribe_mutation('a', _increment_counter)

state['a'] = 2 # increment my_counter
state['a'] = 3 # increment my_counter
state['a'] = 2 # do nothing
```
</CodeGroup>

<Tip>
`subscribe_mutation` is compatible with event handler signature. It will accept all the arguments
of the event handler (`context`, `payload`, ...).
</Tip>

## Receiving a payload

Several events include additional data, known as the event's payload. The event handler can receive that data using the `payload` argument.
Expand Down
24 changes: 24 additions & 0 deletions docs/framework/state-schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ initial_state = wf.init_state({
}, schema=AppSchema)
```

## Calculated properties

Calculated properties are updated automatically when a dependency changes.
They can be used to calculate values derived from application state.

```python
class MyAppState(wf.State):
counter: List[int]

class MyState(wf.WriterState):
counter: List[int]

@wf.property(['counter', 'app.counter'])
def total_counter(self):
return sum(self.counter) + sum(self.app.counter)

initial_state = wf.init_state({
"counter": 0,
"my_app": {
"counter": 0
}
}, schema=MyState)
```

## Multi-level dictionary

Some components like _Vega Lite Chart_ require specifying a graph in the form of a multi-level dictionary.
Expand Down
4 changes: 4 additions & 0 deletions src/writer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
session_manager,
session_verifier,
)
from writer.core import (
writerproperty as property,
)
from writer.ui import WriterUIManager

VERSION = importlib.metadata.version("writer")
Expand Down Expand Up @@ -94,6 +97,7 @@ def init_state(raw_state: Dict[str, Any], schema: Optional[Type[S]] = None) -> U
raise ValueError("Root schema must inherit from WriterState")

_initial_state: S = new_initial_state(concrete_schema, raw_state)

return _initial_state


Expand Down
133 changes: 67 additions & 66 deletions 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, MiddlewareRegistry, WriterSession
from writer.core import EventHandlerRegistry, MiddlewareRegistry, WriterSession, use_request_context
from writer.core_ui import ingest_bmc_component_tree
from writer.ss_types import (
AppProcessServerRequest,
Expand Down Expand Up @@ -232,71 +232,72 @@ def _handle_message(self, session_id: str, request: AppProcessServerRequest) ->
"""
import writer

session = None
type = request.type

if type == "sessionInit":
si_req_payload = InitSessionRequestPayload.parse_obj(
request.payload)
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=self._handle_session_init(si_req_payload)
)

session = writer.session_manager.get_session(session_id)
if not session:
raise MessageHandlingException("Session not found.")
session.update_last_active_timestamp()

if type == "checkSession":
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=None
)

if type == "event":
ev_req_payload = WriterEvent.parse_obj(request.payload)
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=self._handle_event(session, ev_req_payload)
)

if type == "stateEnquiry":
return AppProcessServerResponse(
status="ok",
status_message=None,
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(
status="ok",
status_message=None,
payload=None
)

if self.mode == "edit" and type == "componentUpdate":
cu_req_payload = ComponentUpdateRequestPayload.parse_obj(
request.payload)
self._handle_component_update(session, cu_req_payload)
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=None
)

raise MessageHandlingException("Invalid event.")
with use_request_context(session_id, request):
session = None
type = request.type

if type == "sessionInit":
si_req_payload = InitSessionRequestPayload.parse_obj(
request.payload)
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=self._handle_session_init(si_req_payload)
)

session = writer.session_manager.get_session(session_id)
if not session:
raise MessageHandlingException("Session not found.")
session.update_last_active_timestamp()

if type == "checkSession":
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=None
)

if type == "event":
ev_req_payload = WriterEvent.parse_obj(request.payload)
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=self._handle_event(session, ev_req_payload)
)

if type == "stateEnquiry":
return AppProcessServerResponse(
status="ok",
status_message=None,
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(
status="ok",
status_message=None,
payload=None
)

if self.mode == "edit" and type == "componentUpdate":
cu_req_payload = ComponentUpdateRequestPayload.parse_obj(
request.payload)
self._handle_component_update(session, cu_req_payload)
return AppProcessServerResponse(
status="ok",
status_message=None,
payload=None
)

raise MessageHandlingException("Invalid event.")

def _execute_user_code(self) -> None:
"""
Expand Down
Loading

0 comments on commit 505f6f0

Please sign in to comment.