Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: trigger a calculated property on mutation #496

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

FabienArcellier marked this conversation as resolved.
Show resolved Hide resolved
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
Loading