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

Typed event #366

Merged
merged 7 commits into from
Dec 4, 2024
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented in this file.

## [1.0.20] - UNRELEASED

### Added

- Added `events` module


## [1.0.19] - 26-11-24

### Changed
Expand Down
157 changes: 157 additions & 0 deletions dash_extensions/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import datetime
from enum import Enum
import re
from typing import Optional, Type, TypeVar
from logging import getLogger
from dash import set_props, ctx

from pydantic import BaseModel, Field
from dash_extensions.enrich import Input, dcc
from dash.dependencies import DashDependency

logger = getLogger(__name__)


T = TypeVar("T", bound=DashDependency)


class EventModel(BaseModel):
"""
The EventModel class provides the main interface for creating and dispatching events.
"""

timestamp: float = Field(default_factory=datetime.datetime.now().timestamp)

def add_listener(self) -> Input:
"""
Listen for event.
"""
return self.get_dependency(Input)

def dispatch(self):
"""
Dispatch event.
"""
if self._component_id not in _event_registry:
logger.warning(f"No listener registered for {self._uid}. Event dispatch suppressed.")
return
set_props(self._component_id, {"data": self.model_dump()})

def get_dependency(self, dependency_type: Type[T]) -> T:
"""
Get DashDependency (Input, Output, State) for event.
"""
_register_event(self) # TODO: Could also be on init?
return dependency_type(self._component_id, "data")

def is_trigger(self) -> bool:
return self._component_id in ctx.triggered_prop_ids.values()

@property
def _uid(self) -> str:
raise NotImplementedError

@property
def _component_id(self) -> str:
return f"event_store_{self._uid}"


# region Simple interface (str, Enum; without payload)


class SimpleEvent(EventModel):
"""
Wrapper class for simple events.
"""

type: str | Enum

@property
def _uid(self) -> str:
return _get_event_id(self.type)


def _get_event_id(event: str | Enum) -> str:
if isinstance(event, Enum):
event_tag = event.name.lower()
event_type = _to_snake(event.__class__.__name__)
return f"{event_type}_{event_tag}"
return event


def _to_snake(camel: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", camel).lower()


def _base_event(event: str | Enum) -> SimpleEvent:
return SimpleEvent(type=event)


def dispatch_event(event: str | Enum):
"""
Dispatch an event.
"""
return _base_event(event).dispatch()


def get_event_dependency(event: str | Enum, dependency_type: Type[T]) -> T:
"""
Get DashDependency (Input, Output, State) for a particular event type.
"""
return _base_event(event).get_dependency(dependency_type)


def add_event_listener(event: str | Enum) -> Input:
"""
Listen for a particular event type.
"""
return _base_event(event).get_dependency(Input)


def register_event(event: str | Enum):
"""
Register an event.
"""
_register_event(_base_event(event))


def is_event_trigger(event: str | Enum | list[str | Enum]) -> bool:
"""
Check if an event is a trigger of the callback.
"""
events = [event] if not isinstance(event, list) else event
return get_event_trigger(events) is not None


def get_event_trigger(events: list[str | Enum]) -> Optional[str | Enum]:
"""
Get the event that triggered the callback.
"""
for event in events:
if _base_event(event).is_trigger():
return event
return None


# endregion

# region Event registry


def resolve_event_components() -> list[dcc.Store]:
"""
Returns the components that must be included in the layout.
"""
return [dcc.Store(id=component_id, storage_type="memory") for component_id in list(_event_registry)]


def _register_event(event: EventModel):
_event_registry.add(event._component_id)


"""
Registry over all events that have dependencies.
"""
_event_registry: set[str] = set()

# endregion
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-extensions",
"version": "1.0.19",
"version": "1.0.20",
"description": "Extensions for Plotly Dash.",
"main": "build/index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "dash-extensions"
version = "1.0.19"
version = "1.0.20"
description = "Extensions for Plotly Dash."
authors = ["emher <emil.h.eriksen@gmail.com>"]
license = "MIT"
Expand Down
Loading