From 13d2e4d66924392909edf695ecf7f6f08d6c0367 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Wed, 2 Apr 2025 17:36:46 -0600 Subject: [PATCH 01/15] Break up event model classes into domain-specific category submodules --- streamdeck/manager.py | 3 +- streamdeck/models/events.py | 306 ------------------ streamdeck/models/events/__init__.py | 84 +++++ streamdeck/models/events/adapter.py | 102 ++++++ streamdeck/models/events/application.py | 19 ++ streamdeck/models/events/base.py | 56 ++++ streamdeck/models/events/common.py | 17 + streamdeck/models/events/deep_link.py | 8 + streamdeck/models/events/devices.py | 14 + streamdeck/models/events/dials.py | 21 ++ streamdeck/models/events/keys.py | 16 + .../models/events/property_inspector.py | 19 ++ streamdeck/models/events/settings.py | 16 + streamdeck/models/events/system.py | 9 + streamdeck/models/events/title_parameters.py | 34 ++ streamdeck/models/events/touch_tap.py | 11 + streamdeck/models/events/visibility.py | 16 + streamdeck/websocket.py | 4 +- .../test_action_event_handler_filtering.py | 14 +- tests/actions/test_action_registry.py | 7 +- tests/actions/test_actions.py | 2 +- tests/models/test_event_adapter.py | 7 +- 22 files changed, 463 insertions(+), 322 deletions(-) delete mode 100644 streamdeck/models/events.py create mode 100644 streamdeck/models/events/__init__.py create mode 100644 streamdeck/models/events/adapter.py create mode 100644 streamdeck/models/events/application.py create mode 100644 streamdeck/models/events/base.py create mode 100644 streamdeck/models/events/common.py create mode 100644 streamdeck/models/events/deep_link.py create mode 100644 streamdeck/models/events/devices.py create mode 100644 streamdeck/models/events/dials.py create mode 100644 streamdeck/models/events/keys.py create mode 100644 streamdeck/models/events/property_inspector.py create mode 100644 streamdeck/models/events/settings.py create mode 100644 streamdeck/models/events/system.py create mode 100644 streamdeck/models/events/title_parameters.py create mode 100644 streamdeck/models/events/touch_tap.py create mode 100644 streamdeck/models/events/visibility.py diff --git a/streamdeck/manager.py b/streamdeck/manager.py index e74ff63..7746a8d 100644 --- a/streamdeck/manager.py +++ b/streamdeck/manager.py @@ -9,7 +9,8 @@ from streamdeck.actions import Action, ActionBase, ActionRegistry from streamdeck.command_sender import StreamDeckCommandSender from streamdeck.event_listener import EventListener, EventListenerManager -from streamdeck.models.events import ContextualEventMixin, EventAdapter +from streamdeck.models.events.adapter import EventAdapter +from streamdeck.models.events.common import ContextualEventMixin from streamdeck.types import ( EventHandlerBasicFunc, EventHandlerFunc, diff --git a/streamdeck/models/events.py b/streamdeck/models/events.py deleted file mode 100644 index b13789d..0000000 --- a/streamdeck/models/events.py +++ /dev/null @@ -1,306 +0,0 @@ -from __future__ import annotations - -from abc import ABC -from typing import Annotated, Any, Final, Literal, Union, get_args, get_type_hints - -from pydantic import BaseModel, ConfigDict, Field, TypeAdapter -from typing_extensions import LiteralString, TypedDict, TypeIs # noqa: UP035 - - -# TODO: Create more explicitly-defined payload objects. - - -class EventBase(BaseModel, ABC): - """Base class for event models that represent Stream Deck Plugin SDK events.""" - # Configure to use the docstrings of the fields as the field descriptions. - model_config = ConfigDict(use_attribute_docstrings=True) - - event: str - """Name of the event used to identify what occurred. - - Subclass models must define this field as a Literal type with the event name string that the model represents. - """ - - def __init_subclass__(cls, **kwargs: Any) -> None: - """Validate that the event field is a Literal[str] type.""" - super().__init_subclass__(**kwargs) - - model_event_type = get_type_hints(cls)["event"] - - if not is_literal_str_type(model_event_type): - msg = f"The event field annotation must be a Literal[str] type. Given type: {model_event_type}" - raise TypeError(msg) - - @classmethod - def get_model_event_name(cls) -> tuple[str, ...]: - """Get the value of the subclass model's event field Literal annotation.""" - model_event_type = get_type_hints(cls)["event"] - - # Ensure that the event field annotation is a Literal type. - if not is_literal_str_type(model_event_type): - msg = "The `event` field annotation of an Event model must be a Literal[str] type." - raise TypeError(msg) - - return get_args(model_event_type) - - -def is_literal_str_type(value: object | None) -> TypeIs[LiteralString]: - """Check if a type is a Literal type.""" - if value is None: - return False - - event_field_base_type = getattr(value, "__origin__", None) - - if event_field_base_type is not Literal: - return False - - return all(isinstance(literal_value, str) for literal_value in get_args(value)) - - -## Mixin classes for common event model fields. - -class ContextualEventMixin: - """Mixin class for event models that have action and context fields.""" - action: str - """Unique identifier of the action""" - context: str - """Identifies the instance of an action that caused the event, i.e. the specific key or dial.""" - -class DeviceSpecificEventMixin: - """Mixin class for event models that have a device field.""" - device: str - """Unique identifier of the Stream Deck device that this event is associated with.""" - - -## EventBase implementation models of the Stream Deck Plugin SDK events. - -class ApplicationDidLaunch(EventBase): - event: Literal["applicationDidLaunch"] # type: ignore[override] - payload: dict[Literal["application"], str] - """Payload containing the name of the application that triggered the event.""" - - -class ApplicationDidTerminate(EventBase): - event: Literal["applicationDidTerminate"] # type: ignore[override] - payload: dict[Literal["application"], str] - """Payload containing the name of the application that triggered the event.""" - - -class DeviceDidConnect(EventBase, DeviceSpecificEventMixin): - event: Literal["deviceDidConnect"] # type: ignore[override] - deviceInfo: dict[str, Any] - """Information about the newly connected device.""" - - -class DeviceDidDisconnect(EventBase, DeviceSpecificEventMixin): - event: Literal["deviceDidDisconnect"] # type: ignore[override] - - -class DialDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["dialDown"] # type: ignore[override] - payload: dict[str, Any] - - -class DialRotate(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["dialRotate"] # type: ignore[override] - payload: dict[str, Any] - - -class DialUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["dialUp"] # type: ignore[override] - payload: dict[str, Any] - - -class DidReceiveDeepLink(EventBase): - event: Literal["didReceiveDeepLink"] # type: ignore[override] - payload: dict[Literal["url"], str] - - -class DidReceiveGlobalSettings(EventBase): - event: Literal["didReceiveGlobalSettings"] # type: ignore[override] - payload: dict[Literal["settings"], dict[str, Any]] - - -class DidReceivePropertyInspectorMessage(EventBase, ContextualEventMixin): - event: Literal["sendToPlugin"] # type: ignore[override] - payload: dict[str, Any] - - -class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["didReceiveSettings"] # type: ignore[override] - payload: dict[str, Any] - - -class KeyDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["keyDown"] # type: ignore[override] - payload: dict[str, Any] - - -class KeyUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["keyUp"] # type: ignore[override] - payload: dict[str, Any] - - -class PropertyInspectorDidAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["propertyInspectorDidAppear"] # type: ignore[override] - - -class PropertyInspectorDidDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["propertyInspectorDidDisappear"] # type: ignore[override] - - -class SystemDidWakeUp(EventBase): - event: Literal["systemDidWakeUp"] # type: ignore[override] - - -class TitleParametersDict(TypedDict): - fontFamily: str - fontSize: int - fontStyle: str - fontUnderline: bool - showTitle: bool - titleAlignment: Literal["top", "middle", "bottom"] - titleColor: str - - -class TitleParametersDidChangePayload(TypedDict): - controller: Literal["Keypad", "Encoder"] - coordinates: dict[Literal["column", "row"], int] - settings: dict[str, Any] - state: int - title: str - titleParameters: TitleParametersDict - - -class TitleParametersDidChange(EventBase, DeviceSpecificEventMixin): - event: Literal["titleParametersDidChange"] # type: ignore[override] - context: str - """Identifies the instance of an action that caused the event, i.e. the specific key or dial.""" - payload: TitleParametersDidChangePayload - - -class TouchTap(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["touchTap"] # type: ignore[override] - payload: dict[str, Any] - - -class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["willAppear"] # type: ignore[override] - payload: dict[str, Any] - - -class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): - event: Literal["willDisappear"] # type: ignore[override] - payload: dict[str, Any] - - -## Default event models and names. - - -event_adapter: TypeAdapter[EventBase] = TypeAdapter( - Annotated[ - Union[ # noqa: UP007 - ApplicationDidLaunch, - ApplicationDidTerminate, - DeviceDidConnect, - DeviceDidDisconnect, - DialDown, - DialRotate, - DialUp, - DidReceiveDeepLink, - KeyUp, - KeyDown, - DidReceivePropertyInspectorMessage, - PropertyInspectorDidAppear, - PropertyInspectorDidDisappear, - DidReceiveGlobalSettings, - DidReceiveSettings, - SystemDidWakeUp, - TitleParametersDidChange, - TouchTap, - WillAppear, - WillDisappear, - ], - Field(discriminator="event") - ] -) - - -DEFAULT_EVENT_MODELS: Final[list[type[EventBase]]] = [ - ApplicationDidLaunch, - ApplicationDidTerminate, - DeviceDidConnect, - DeviceDidDisconnect, - DialDown, - DialRotate, - DialUp, - DidReceiveDeepLink, - KeyUp, - KeyDown, - DidReceivePropertyInspectorMessage, - PropertyInspectorDidAppear, - PropertyInspectorDidDisappear, - DidReceiveGlobalSettings, - DidReceiveSettings, - SystemDidWakeUp, - TitleParametersDidChange, - TouchTap, - WillAppear, - WillDisappear, -] - - -def _get_default_event_names() -> set[str]: - default_event_names: set[str] = set() - - for event_model in DEFAULT_EVENT_MODELS: - default_event_names.update(event_model.get_model_event_name()) - - return default_event_names - - -DEFAULT_EVENT_NAMES: Final[set[str]] = _get_default_event_names() - - -## EventAdapter class for handling and extending available event models. - -class EventAdapter: - """TypeAdapter-encompassing class for handling and extending available event models.""" - def __init__(self) -> None: - self._models: list[type[EventBase]] = [] - self._type_adapter: TypeAdapter[EventBase] | None = None - - self._event_names: set[str] = set() - """A set of all event names that have been registered with the adapter. - This set starts out containing the default event models defined by the library. - """ - - for model in DEFAULT_EVENT_MODELS: - self.add_model(model) - - def add_model(self, model: type[EventBase]) -> None: - """Add a model to the adapter, and add the event name of the model to the set of registered event names.""" - self._models.append(model) - self._event_names.update(model.get_model_event_name()) - - def event_name_exists(self, event_name: str) -> bool: - """Check if an event name has been registered with the adapter.""" - return event_name in self._event_names - - @property - def type_adapter(self) -> TypeAdapter[EventBase]: - """Get the TypeAdapter instance for the event models.""" - if self._type_adapter is None: - self._type_adapter = TypeAdapter( - Annotated[ - Union[tuple(self._models)], # noqa: UP007 - Field(discriminator="event") - ] - ) - - return self._type_adapter - - def validate_json(self, data: str | bytes) -> EventBase: - """Validate a JSON string or bytes object as an event model.""" - return self.type_adapter.validate_json(data) - diff --git a/streamdeck/models/events/__init__.py b/streamdeck/models/events/__init__.py new file mode 100644 index 0000000..bb98daa --- /dev/null +++ b/streamdeck/models/events/__init__.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from typing import Final + +from streamdeck.models.events.application import ApplicationDidLaunch, ApplicationDidTerminate +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.deep_link import DidReceiveDeepLink +from streamdeck.models.events.devices import DeviceDidConnect, DeviceDidDisconnect +from streamdeck.models.events.dials import DialDown, DialRotate, DialUp +from streamdeck.models.events.keys import KeyDown, KeyUp +from streamdeck.models.events.property_inspector import ( + DidReceivePropertyInspectorMessage, + PropertyInspectorDidAppear, + PropertyInspectorDidDisappear, +) +from streamdeck.models.events.settings import DidReceiveGlobalSettings, DidReceiveSettings +from streamdeck.models.events.system import SystemDidWakeUp +from streamdeck.models.events.title_parameters import TitleParametersDidChange +from streamdeck.models.events.touch_tap import TouchTap +from streamdeck.models.events.visibility import WillAppear, WillDisappear + + +DEFAULT_EVENT_MODELS: Final[list[type[EventBase]]] = [ + ApplicationDidLaunch, + ApplicationDidTerminate, + DeviceDidConnect, + DeviceDidDisconnect, + DialDown, + DialRotate, + DialUp, + DidReceiveDeepLink, + KeyUp, + KeyDown, + DidReceivePropertyInspectorMessage, + PropertyInspectorDidAppear, + PropertyInspectorDidDisappear, + DidReceiveGlobalSettings, + DidReceiveSettings, + SystemDidWakeUp, + TitleParametersDidChange, + TouchTap, + WillAppear, + WillDisappear, +] + + +def _get_default_event_names() -> set[str]: + default_event_names: set[str] = set() + + for event_model in DEFAULT_EVENT_MODELS: + default_event_names.update(event_model.get_model_event_name()) + + return default_event_names + + +DEFAULT_EVENT_NAMES: Final[set[str]] = _get_default_event_names() + + + +__all__ = [ + "DEFAULT_EVENT_MODELS", + "DEFAULT_EVENT_NAMES", + "ApplicationDidLaunch", + "ApplicationDidTerminate", + "DeviceDidConnect", + "DeviceDidDisconnect", + "DialDown", + "DialRotate", + "DialUp", + "DidReceiveDeepLink", + "DidReceiveGlobalSettings", + "DidReceivePropertyInspectorMessage", + "DidReceiveSettings", + "EventBase", + "KeyDown", + "KeyUp", + "PropertyInspectorDidAppear", + "PropertyInspectorDidDisappear", + "SystemDidWakeUp", + "TitleParametersDidChange", + "TouchTap", + "WillAppear", + "WillDisappear", +] diff --git a/streamdeck/models/events/adapter.py b/streamdeck/models/events/adapter.py new file mode 100644 index 0000000..cd0a305 --- /dev/null +++ b/streamdeck/models/events/adapter.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Annotated, Union + +from pydantic import Field, TypeAdapter + +from streamdeck.models.events import DEFAULT_EVENT_MODELS +from streamdeck.models.events.application import ApplicationDidLaunch, ApplicationDidTerminate +from streamdeck.models.events.deep_link import DidReceiveDeepLink +from streamdeck.models.events.devices import DeviceDidConnect, DeviceDidDisconnect +from streamdeck.models.events.dials import DialDown, DialRotate, DialUp +from streamdeck.models.events.keys import KeyDown, KeyUp +from streamdeck.models.events.property_inspector import ( + DidReceivePropertyInspectorMessage, + PropertyInspectorDidAppear, + PropertyInspectorDidDisappear, +) +from streamdeck.models.events.settings import DidReceiveGlobalSettings, DidReceiveSettings +from streamdeck.models.events.system import SystemDidWakeUp +from streamdeck.models.events.title_parameters import TitleParametersDidChange +from streamdeck.models.events.touch_tap import TouchTap +from streamdeck.models.events.visibility import WillAppear, WillDisappear + + +if TYPE_CHECKING: + from streamdeck.models.events.base import EventBase + + +## Default event models and names. + + +event_adapter: TypeAdapter[EventBase] = TypeAdapter( + Annotated[ + Union[ # noqa: UP007 + ApplicationDidLaunch, + ApplicationDidTerminate, + DeviceDidConnect, + DeviceDidDisconnect, + DialDown, + DialRotate, + DialUp, + DidReceiveDeepLink, + KeyUp, + KeyDown, + DidReceivePropertyInspectorMessage, + PropertyInspectorDidAppear, + PropertyInspectorDidDisappear, + DidReceiveGlobalSettings, + DidReceiveSettings, + SystemDidWakeUp, + TitleParametersDidChange, + TouchTap, + WillAppear, + WillDisappear, + ], + Field(discriminator="event") + ] +) + + +## EventAdapter class for handling and extending available event models. + +class EventAdapter: + """TypeAdapter-encompassing class for handling and extending available event models.""" + def __init__(self) -> None: + self._models: list[type[EventBase]] = [] + self._type_adapter: TypeAdapter[EventBase] | None = None + + self._event_names: set[str] = set() + """A set of all event names that have been registered with the adapter. + This set starts out containing the default event models defined by the library. + """ + + for model in DEFAULT_EVENT_MODELS: + self.add_model(model) + + def add_model(self, model: type[EventBase]) -> None: + """Add a model to the adapter, and add the event name of the model to the set of registered event names.""" + self._models.append(model) + self._event_names.update(model.get_model_event_name()) + + def event_name_exists(self, event_name: str) -> bool: + """Check if an event name has been registered with the adapter.""" + return event_name in self._event_names + + @property + def type_adapter(self) -> TypeAdapter[EventBase]: + """Get the TypeAdapter instance for the event models.""" + if self._type_adapter is None: + self._type_adapter = TypeAdapter( + Annotated[ + Union[tuple(self._models)], # noqa: UP007 + Field(discriminator="event") + ] + ) + + return self._type_adapter + + def validate_json(self, data: str | bytes) -> EventBase: + """Validate a JSON string or bytes object as an event model.""" + return self.type_adapter.validate_json(data) + diff --git a/streamdeck/models/events/application.py b/streamdeck/models/events/application.py new file mode 100644 index 0000000..aae80de --- /dev/null +++ b/streamdeck/models/events/application.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import Literal + +from streamdeck.models.events.base import EventBase + + +## EventBase implementation models of the Stream Deck Plugin SDK events. + +class ApplicationDidLaunch(EventBase): + event: Literal["applicationDidLaunch"] # type: ignore[override] + payload: dict[Literal["application"], str] + """Payload containing the name of the application that triggered the event.""" + + +class ApplicationDidTerminate(EventBase): + event: Literal["applicationDidTerminate"] # type: ignore[override] + payload: dict[Literal["application"], str] + """Payload containing the name of the application that triggered the event.""" diff --git a/streamdeck/models/events/base.py b/streamdeck/models/events/base.py new file mode 100644 index 0000000..529c210 --- /dev/null +++ b/streamdeck/models/events/base.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from abc import ABC +from typing import Any, Literal, get_args, get_type_hints + +from pydantic import BaseModel, ConfigDict +from typing_extensions import LiteralString, TypeIs # noqa: UP035 + + +def is_literal_str_type(value: object | None) -> TypeIs[LiteralString]: + """Check if a type is a Literal type.""" + if value is None: + return False + + event_field_base_type = getattr(value, "__origin__", None) + + if event_field_base_type is not Literal: + return False + + return all(isinstance(literal_value, str) for literal_value in get_args(value)) + + +## EventBase implementation models of the Stream Deck Plugin SDK events. + +class EventBase(BaseModel, ABC): + """Base class for event models that represent Stream Deck Plugin SDK events.""" + # Configure to use the docstrings of the fields as the field descriptions. + model_config = ConfigDict(use_attribute_docstrings=True) + + event: str + """Name of the event used to identify what occurred. + + Subclass models must define this field as a Literal type with the event name string that the model represents. + """ + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Validate that the event field is a Literal[str] type.""" + super().__init_subclass__(**kwargs) + + model_event_type = get_type_hints(cls)["event"] + + if not is_literal_str_type(model_event_type): + msg = f"The event field annotation must be a Literal[str] type. Given type: {model_event_type}" + raise TypeError(msg) + + @classmethod + def get_model_event_name(cls) -> tuple[str, ...]: + """Get the value of the subclass model's event field Literal annotation.""" + model_event_type = get_type_hints(cls)["event"] + + # Ensure that the event field annotation is a Literal type. + if not is_literal_str_type(model_event_type): + msg = "The `event` field annotation of an Event model must be a Literal[str] type." + raise TypeError(msg) + + return get_args(model_event_type) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py new file mode 100644 index 0000000..4e5246f --- /dev/null +++ b/streamdeck/models/events/common.py @@ -0,0 +1,17 @@ +from __future__ import annotations + + +## Mixin classes for common event model fields. + +class ContextualEventMixin: + """Mixin class for event models that have action and context fields.""" + action: str + """Unique identifier of the action""" + context: str + """Identifies the instance of an action that caused the event, i.e. the specific key or dial.""" + + +class DeviceSpecificEventMixin: + """Mixin class for event models that have a device field.""" + device: str + """Unique identifier of the Stream Deck device that this event is associated with.""" diff --git a/streamdeck/models/events/deep_link.py b/streamdeck/models/events/deep_link.py new file mode 100644 index 0000000..927bf4c --- /dev/null +++ b/streamdeck/models/events/deep_link.py @@ -0,0 +1,8 @@ +from typing import Literal + +from streamdeck.models.events.base import EventBase + + +class DidReceiveDeepLink(EventBase): + event: Literal["didReceiveDeepLink"] # type: ignore[override] + payload: dict[Literal["url"], str] diff --git a/streamdeck/models/events/devices.py b/streamdeck/models/events/devices.py new file mode 100644 index 0000000..417a13a --- /dev/null +++ b/streamdeck/models/events/devices.py @@ -0,0 +1,14 @@ +from typing import Any, Literal + +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.common import DeviceSpecificEventMixin + + +class DeviceDidConnect(EventBase, DeviceSpecificEventMixin): + event: Literal["deviceDidConnect"] # type: ignore[override] + deviceInfo: dict[str, Any] + """Information about the newly connected device.""" + + +class DeviceDidDisconnect(EventBase, DeviceSpecificEventMixin): + event: Literal["deviceDidDisconnect"] # type: ignore[override] diff --git a/streamdeck/models/events/dials.py b/streamdeck/models/events/dials.py new file mode 100644 index 0000000..a655a5e --- /dev/null +++ b/streamdeck/models/events/dials.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import Any, Literal + +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin + + +class DialDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["dialDown"] # type: ignore[override] + payload: dict[str, Any] + + +class DialRotate(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["dialRotate"] # type: ignore[override] + payload: dict[str, Any] + + +class DialUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["dialUp"] # type: ignore[override] + payload: dict[str, Any] diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py new file mode 100644 index 0000000..45aa839 --- /dev/null +++ b/streamdeck/models/events/keys.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any, Literal + +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin + + +class KeyDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["keyDown"] # type: ignore[override] + payload: dict[str, Any] + + +class KeyUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["keyUp"] # type: ignore[override] + payload: dict[str, Any] diff --git a/streamdeck/models/events/property_inspector.py b/streamdeck/models/events/property_inspector.py new file mode 100644 index 0000000..e610b0a --- /dev/null +++ b/streamdeck/models/events/property_inspector.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import Any, Literal + +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin + + +class DidReceivePropertyInspectorMessage(EventBase, ContextualEventMixin): + event: Literal["sendToPlugin"] # type: ignore[override] + payload: dict[str, Any] + + +class PropertyInspectorDidAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["propertyInspectorDidAppear"] # type: ignore[override] + + +class PropertyInspectorDidDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["propertyInspectorDidDisappear"] # type: ignore[override] diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py new file mode 100644 index 0000000..c3e5bcb --- /dev/null +++ b/streamdeck/models/events/settings.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any, Literal + +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin + + +class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["didReceiveSettings"] # type: ignore[override] + payload: dict[str, Any] + + +class DidReceiveGlobalSettings(EventBase): + event: Literal["didReceiveGlobalSettings"] # type: ignore[override] + payload: dict[Literal["settings"], dict[str, Any]] diff --git a/streamdeck/models/events/system.py b/streamdeck/models/events/system.py new file mode 100644 index 0000000..8439cd8 --- /dev/null +++ b/streamdeck/models/events/system.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from typing import Literal + +from streamdeck.models.events.base import EventBase + + +class SystemDidWakeUp(EventBase): + event: Literal["systemDidWakeUp"] # type: ignore[override] diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py new file mode 100644 index 0000000..4fe61a0 --- /dev/null +++ b/streamdeck/models/events/title_parameters.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import Any, Literal + +from typing_extensions import TypedDict + +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.common import DeviceSpecificEventMixin + + +class TitleParametersDict(TypedDict): + fontFamily: str + fontSize: int + fontStyle: str + fontUnderline: bool + showTitle: bool + titleAlignment: Literal["top", "middle", "bottom"] + titleColor: str + + +class TitleParametersDidChangePayload(TypedDict): + controller: Literal["Keypad", "Encoder"] + coordinates: dict[Literal["column", "row"], int] + settings: dict[str, Any] + state: int + title: str + titleParameters: TitleParametersDict + + +class TitleParametersDidChange(EventBase, DeviceSpecificEventMixin): + event: Literal["titleParametersDidChange"] # type: ignore[override] + context: str + """Identifies the instance of an action that caused the event, i.e. the specific key or dial.""" + payload: TitleParametersDidChangePayload diff --git a/streamdeck/models/events/touch_tap.py b/streamdeck/models/events/touch_tap.py new file mode 100644 index 0000000..e1561d5 --- /dev/null +++ b/streamdeck/models/events/touch_tap.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from typing import Any, Literal + +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin + + +class TouchTap(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["touchTap"] # type: ignore[override] + payload: dict[str, Any] diff --git a/streamdeck/models/events/visibility.py b/streamdeck/models/events/visibility.py new file mode 100644 index 0000000..93d5862 --- /dev/null +++ b/streamdeck/models/events/visibility.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Any, Literal + +from streamdeck.models.events.base import EventBase +from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin + + +class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["willAppear"] # type: ignore[override] + payload: dict[str, Any] + + +class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + event: Literal["willDisappear"] # type: ignore[override] + payload: dict[str, Any] diff --git a/streamdeck/websocket.py b/streamdeck/websocket.py index 8fa0504..113b9be 100644 --- a/streamdeck/websocket.py +++ b/streamdeck/websocket.py @@ -13,7 +13,7 @@ from websockets.sync.client import ClientConnection, connect from streamdeck.event_listener import EventListener, StopStreaming -from streamdeck.models import events +from streamdeck.models.events import DEFAULT_EVENT_MODELS, EventBase if TYPE_CHECKING: @@ -32,7 +32,7 @@ class WebSocketClient(EventListener): """A client for connecting to the Stream Deck device's WebSocket server and sending/receiving events.""" _client: ClientConnection | None - event_models: ClassVar[list[type[events.EventBase]]] = events.DEFAULT_EVENT_MODELS + event_models: ClassVar[list[type[EventBase]]] = DEFAULT_EVENT_MODELS def __init__(self, port: int): """Initialize a WebSocketClient instance. diff --git a/tests/actions/test_action_event_handler_filtering.py b/tests/actions/test_action_event_handler_filtering.py index 4c5e315..d55915d 100644 --- a/tests/actions/test_action_event_handler_filtering.py +++ b/tests/actions/test_action_event_handler_filtering.py @@ -4,9 +4,8 @@ from unittest.mock import create_autospec import pytest -from polyfactory.factories.pydantic_factory import ModelFactory from streamdeck.actions import Action, ActionRegistry, GlobalAction -from streamdeck.models import events +from streamdeck.models.events.common import ContextualEventMixin from tests.test_utils.fake_event_factories import ( ApplicationDidLaunchEventFactory, @@ -18,6 +17,9 @@ if TYPE_CHECKING: from unittest.mock import Mock + from polyfactory.factories.pydantic_factory import ModelFactory + from streamdeck.models import events + @pytest.fixture @@ -34,7 +36,7 @@ def dummy_handler(event: events.EventBase) -> None: ApplicationDidLaunchEventFactory ]) def fake_event_data(request: pytest.FixtureRequest) -> events.EventBase: - event_factory = cast(ModelFactory[events.EventBase], request.param) + event_factory = cast("ModelFactory[events.EventBase]", request.param) return event_factory.build() @@ -67,7 +69,7 @@ def test_action_gets_triggered_by_event( Actions should only be triggered by events that have the same unique identifier properties as the action. """ # Extract the action UUID from the fake event data, or use a default value - action_uuid: str = fake_event_data.action if isinstance(fake_event_data, events.ContextualEventMixin) else "my-fake-action-uuid" + action_uuid: str = fake_event_data.action if isinstance(fake_event_data, ContextualEventMixin) else "my-fake-action-uuid" action = Action(uuid=action_uuid) @@ -90,7 +92,7 @@ def test_global_action_registry_get_action_handlers_filtering( fake_event_data: events.EventBase, ) -> None: # Extract the action UUID from the fake event data, or use a default value - action_uuid: str | None = fake_event_data.action if isinstance(fake_event_data, events.ContextualEventMixin) else None + action_uuid: str | None = fake_event_data.action if isinstance(fake_event_data, ContextualEventMixin) else None registry = ActionRegistry() # Create an Action instance, without an action UUID as global actions aren't associated with a specific action @@ -117,7 +119,7 @@ def test_action_registry_get_action_handlers_filtering( fake_event_data: events.EventBase, ) -> None: # Extract the action UUID from the fake event data, or use a default value - action_uuid: str | None = fake_event_data.action if isinstance(fake_event_data, events.ContextualEventMixin) else None + action_uuid: str | None = fake_event_data.action if isinstance(fake_event_data, ContextualEventMixin) else None registry = ActionRegistry() # Create an Action instance, using either the fake event's action UUID or a default value diff --git a/tests/actions/test_action_registry.py b/tests/actions/test_action_registry.py index 0909ef2..606d820 100644 --- a/tests/actions/test_action_registry.py +++ b/tests/actions/test_action_registry.py @@ -1,8 +1,9 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest from streamdeck.actions import Action, ActionRegistry -from streamdeck.models import events from tests.test_utils.fake_event_factories import ( DialDownEventFactory, @@ -11,6 +12,10 @@ ) +if TYPE_CHECKING: + from streamdeck.models import events + + def test_register_action() -> None: """Test that an action can be registered.""" registry = ActionRegistry() diff --git a/tests/actions/test_actions.py b/tests/actions/test_actions.py index 8decc51..85660a4 100644 --- a/tests/actions/test_actions.py +++ b/tests/actions/test_actions.py @@ -17,7 +17,7 @@ def action(request: pytest.FixtureRequest) -> ActionBase: We have to initialize the classes here to ensure fresh instances are used to avoid sharing data between tests. """ - action_class, init_args = cast(tuple[type[ActionBase], tuple[Any]], request.param) + action_class, init_args = cast("tuple[type[ActionBase], tuple[Any]]", request.param) return action_class(*init_args) diff --git a/tests/models/test_event_adapter.py b/tests/models/test_event_adapter.py index efcecc2..a644e07 100644 --- a/tests/models/test_event_adapter.py +++ b/tests/models/test_event_adapter.py @@ -3,12 +3,9 @@ import pytest from pydantic import ValidationError -from streamdeck.models.events import ( - DEFAULT_EVENT_MODELS, - DEFAULT_EVENT_NAMES, +from streamdeck.models.events import DEFAULT_EVENT_MODELS, DEFAULT_EVENT_NAMES, EventBase, KeyDown +from streamdeck.models.events.adapter import ( EventAdapter, - EventBase, - KeyDown, ) from tests.test_utils.fake_event_factories import KeyDownEventFactory From c59f35c26ff0c07567bb7a64f84cb748e838f79e Mon Sep 17 00:00:00 2001 From: strohganoff Date: Wed, 2 Apr 2025 17:54:08 -0600 Subject: [PATCH 02/15] Remove retired TypeAdapter --- streamdeck/models/events/adapter.py | 47 ----------------------------- 1 file changed, 47 deletions(-) diff --git a/streamdeck/models/events/adapter.py b/streamdeck/models/events/adapter.py index cd0a305..d8e135d 100644 --- a/streamdeck/models/events/adapter.py +++ b/streamdeck/models/events/adapter.py @@ -5,59 +5,12 @@ from pydantic import Field, TypeAdapter from streamdeck.models.events import DEFAULT_EVENT_MODELS -from streamdeck.models.events.application import ApplicationDidLaunch, ApplicationDidTerminate -from streamdeck.models.events.deep_link import DidReceiveDeepLink -from streamdeck.models.events.devices import DeviceDidConnect, DeviceDidDisconnect -from streamdeck.models.events.dials import DialDown, DialRotate, DialUp -from streamdeck.models.events.keys import KeyDown, KeyUp -from streamdeck.models.events.property_inspector import ( - DidReceivePropertyInspectorMessage, - PropertyInspectorDidAppear, - PropertyInspectorDidDisappear, -) -from streamdeck.models.events.settings import DidReceiveGlobalSettings, DidReceiveSettings -from streamdeck.models.events.system import SystemDidWakeUp -from streamdeck.models.events.title_parameters import TitleParametersDidChange -from streamdeck.models.events.touch_tap import TouchTap -from streamdeck.models.events.visibility import WillAppear, WillDisappear if TYPE_CHECKING: from streamdeck.models.events.base import EventBase -## Default event models and names. - - -event_adapter: TypeAdapter[EventBase] = TypeAdapter( - Annotated[ - Union[ # noqa: UP007 - ApplicationDidLaunch, - ApplicationDidTerminate, - DeviceDidConnect, - DeviceDidDisconnect, - DialDown, - DialRotate, - DialUp, - DidReceiveDeepLink, - KeyUp, - KeyDown, - DidReceivePropertyInspectorMessage, - PropertyInspectorDidAppear, - PropertyInspectorDidDisappear, - DidReceiveGlobalSettings, - DidReceiveSettings, - SystemDidWakeUp, - TitleParametersDidChange, - TouchTap, - WillAppear, - WillDisappear, - ], - Field(discriminator="event") - ] -) - - ## EventAdapter class for handling and extending available event models. class EventAdapter: From fbb47b629bed22fcdf34ee5c5fd933387440c7b7 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Wed, 2 Apr 2025 17:56:19 -0600 Subject: [PATCH 03/15] Give detailed heredocs to model classes and their attributes --- streamdeck/models/events/__init__.py | 14 +++++++++++++- streamdeck/models/events/application.py | 2 ++ streamdeck/models/events/base.py | 2 +- streamdeck/models/events/deep_link.py | 7 +++++++ streamdeck/models/events/devices.py | 2 ++ streamdeck/models/events/dials.py | 6 ++++++ streamdeck/models/events/keys.py | 4 ++++ streamdeck/models/events/property_inspector.py | 10 ++++++++++ streamdeck/models/events/settings.py | 2 ++ streamdeck/models/events/system.py | 1 + streamdeck/models/events/title_parameters.py | 16 ++++++++++++++++ streamdeck/models/events/touch_tap.py | 2 ++ streamdeck/models/events/visibility.py | 9 +++++++++ 13 files changed, 75 insertions(+), 2 deletions(-) diff --git a/streamdeck/models/events/__init__.py b/streamdeck/models/events/__init__.py index bb98daa..75f86ef 100644 --- a/streamdeck/models/events/__init__.py +++ b/streamdeck/models/events/__init__.py @@ -1,6 +1,14 @@ +"""Stream Deck Event Models. + +These models are used to represent the events that can be received by the Stream Deck SDK plugin. +The "default" events are those passed from the Stream Deck software itself, but custom events can +be created by the plugin developer and listened for in the same way. + +The events are organized into the same categories as the Stream Deck SDK documentation defines them. +""" from __future__ import annotations -from typing import Final +from typing import TYPE_CHECKING from streamdeck.models.events.application import ApplicationDidLaunch, ApplicationDidTerminate from streamdeck.models.events.base import EventBase @@ -20,6 +28,10 @@ from streamdeck.models.events.visibility import WillAppear, WillDisappear +if TYPE_CHECKING: + from typing import Final + + DEFAULT_EVENT_MODELS: Final[list[type[EventBase]]] = [ ApplicationDidLaunch, ApplicationDidTerminate, diff --git a/streamdeck/models/events/application.py b/streamdeck/models/events/application.py index aae80de..1f12c55 100644 --- a/streamdeck/models/events/application.py +++ b/streamdeck/models/events/application.py @@ -8,12 +8,14 @@ ## EventBase implementation models of the Stream Deck Plugin SDK events. class ApplicationDidLaunch(EventBase): + """Occurs when a monitored application is launched.""" event: Literal["applicationDidLaunch"] # type: ignore[override] payload: dict[Literal["application"], str] """Payload containing the name of the application that triggered the event.""" class ApplicationDidTerminate(EventBase): + """Occurs when a monitored application terminates.""" event: Literal["applicationDidTerminate"] # type: ignore[override] payload: dict[Literal["application"], str] """Payload containing the name of the application that triggered the event.""" diff --git a/streamdeck/models/events/base.py b/streamdeck/models/events/base.py index 529c210..fc32ccf 100644 --- a/streamdeck/models/events/base.py +++ b/streamdeck/models/events/base.py @@ -8,7 +8,7 @@ def is_literal_str_type(value: object | None) -> TypeIs[LiteralString]: - """Check if a type is a Literal type.""" + """Check if a type is a Literal type with string values.""" if value is None: return False diff --git a/streamdeck/models/events/deep_link.py b/streamdeck/models/events/deep_link.py index 927bf4c..5b847c5 100644 --- a/streamdeck/models/events/deep_link.py +++ b/streamdeck/models/events/deep_link.py @@ -4,5 +4,12 @@ class DidReceiveDeepLink(EventBase): + """Occurs when Stream Deck receives a deep-link message intended for the plugin. + + The message is re-routed to the plugin, and provided as part of the payload. + One-way deep-link message can be routed to the plugin using the URL format: + streamdeck://plugins/message//{MESSAGE}. + """ event: Literal["didReceiveDeepLink"] # type: ignore[override] payload: dict[Literal["url"], str] + """Payload containing information about the URL that triggered the event.""" diff --git a/streamdeck/models/events/devices.py b/streamdeck/models/events/devices.py index 417a13a..83f224b 100644 --- a/streamdeck/models/events/devices.py +++ b/streamdeck/models/events/devices.py @@ -5,10 +5,12 @@ class DeviceDidConnect(EventBase, DeviceSpecificEventMixin): + """Occurs when a Stream Deck device is connected.""" event: Literal["deviceDidConnect"] # type: ignore[override] deviceInfo: dict[str, Any] """Information about the newly connected device.""" class DeviceDidDisconnect(EventBase, DeviceSpecificEventMixin): + """Occurs when a Stream Deck device is disconnected.""" event: Literal["deviceDidDisconnect"] # type: ignore[override] diff --git a/streamdeck/models/events/dials.py b/streamdeck/models/events/dials.py index a655a5e..3d5ae36 100644 --- a/streamdeck/models/events/dials.py +++ b/streamdeck/models/events/dials.py @@ -7,15 +7,21 @@ class DialDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the user presses a dial (Stream Deck +).""" event: Literal["dialDown"] # type: ignore[override] payload: dict[str, Any] + """Contextualized information for this event.""" class DialRotate(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the user rotates a dial (Stream Deck +).""" event: Literal["dialRotate"] # type: ignore[override] payload: dict[str, Any] + """Contextualized information for this event.""" class DialUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the user releases a pressed dial (Stream Deck +).""" event: Literal["dialUp"] # type: ignore[override] payload: dict[str, Any] + """Contextualized information for this event.""" diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index 45aa839..8ad76ea 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -7,10 +7,14 @@ class KeyDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the user presses a action down.""" event: Literal["keyDown"] # type: ignore[override] payload: dict[str, Any] + """Contextualized information for this event.""" class KeyUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the user releases a pressed action.""" event: Literal["keyUp"] # type: ignore[override] payload: dict[str, Any] + """Contextualized information for this event.""" diff --git a/streamdeck/models/events/property_inspector.py b/streamdeck/models/events/property_inspector.py index e610b0a..6838243 100644 --- a/streamdeck/models/events/property_inspector.py +++ b/streamdeck/models/events/property_inspector.py @@ -7,13 +7,23 @@ class DidReceivePropertyInspectorMessage(EventBase, ContextualEventMixin): + """Occurs when a payload was received from the UI.""" event: Literal["sendToPlugin"] # type: ignore[override] payload: dict[str, Any] + class PropertyInspectorDidAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the property inspector associated with the action becomes visible. + + I.e. the user selected an action in the Stream Deck application. + """ event: Literal["propertyInspectorDidAppear"] # type: ignore[override] class PropertyInspectorDidDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the property inspector associated with the action becomes invisible. + + I.e. the user unselected the action in the Stream Deck application. + """ event: Literal["propertyInspectorDidDisappear"] # type: ignore[override] diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py index c3e5bcb..16fdb3a 100644 --- a/streamdeck/models/events/settings.py +++ b/streamdeck/models/events/settings.py @@ -7,10 +7,12 @@ class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the settings associated with an action instance are requested, or when the the settings were updated by the property inspector.""" event: Literal["didReceiveSettings"] # type: ignore[override] payload: dict[str, Any] class DidReceiveGlobalSettings(EventBase): + """Occurs when the plugin receives the global settings from the Stream Deck.""" event: Literal["didReceiveGlobalSettings"] # type: ignore[override] payload: dict[Literal["settings"], dict[str, Any]] diff --git a/streamdeck/models/events/system.py b/streamdeck/models/events/system.py index 8439cd8..2c6be51 100644 --- a/streamdeck/models/events/system.py +++ b/streamdeck/models/events/system.py @@ -6,4 +6,5 @@ class SystemDidWakeUp(EventBase): + """Occurs when the computer wakes up.""" event: Literal["systemDidWakeUp"] # type: ignore[override] diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index 4fe61a0..bcec6b6 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -9,25 +9,41 @@ class TitleParametersDict(TypedDict): + """Defines aesthetic properties that determine how the title should be rendered.""" fontFamily: str + """Font-family the title will be rendered with.""" fontSize: int + """Font-size the title will be rendered in.""" fontStyle: str + """Typography of the title.""" fontUnderline: bool + """Whether the font should be underlined.""" showTitle: bool + """Whether the user has opted to show, or hide the title for this action instance.""" titleAlignment: Literal["top", "middle", "bottom"] + """Alignment of the title.""" titleColor: str + """Color of the title, represented as a hexadecimal value.""" class TitleParametersDidChangePayload(TypedDict): + """Contextualized information for this event.""" controller: Literal["Keypad", "Encoder"] + """Defines the controller type the action is applicable to.""" coordinates: dict[Literal["column", "row"], int] + """Coordinates that identify the location of an action.""" settings: dict[str, Any] + """Settings associated with the action instance.""" state: int + """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" title: str + """Title of the action, as specified by the user or dynamically by the plugin.""" titleParameters: TitleParametersDict + """Defines aesthetic properties that determine how the title should be rendered.""" class TitleParametersDidChange(EventBase, DeviceSpecificEventMixin): + """Occurs when the user updates an action's title settings in the Stream Deck application.""" event: Literal["titleParametersDidChange"] # type: ignore[override] context: str """Identifies the instance of an action that caused the event, i.e. the specific key or dial.""" diff --git a/streamdeck/models/events/touch_tap.py b/streamdeck/models/events/touch_tap.py index e1561d5..23dac66 100644 --- a/streamdeck/models/events/touch_tap.py +++ b/streamdeck/models/events/touch_tap.py @@ -7,5 +7,7 @@ class TouchTap(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when the user taps the touchscreen (Stream Deck +).""" event: Literal["touchTap"] # type: ignore[override] payload: dict[str, Any] + """Contextualized information for this event.""" diff --git a/streamdeck/models/events/visibility.py b/streamdeck/models/events/visibility.py index 93d5862..1dcdc0b 100644 --- a/streamdeck/models/events/visibility.py +++ b/streamdeck/models/events/visibility.py @@ -7,10 +7,19 @@ class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when an action appears on the Stream Deck due to the user navigating to another page, profile, folder, etc. + + This also occurs during startup if the action is on the "front page". + An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. + """ event: Literal["willAppear"] # type: ignore[override] payload: dict[str, Any] class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): + """Occurs when an action disappears from the Stream Deck due to the user navigating to another page, profile, folder, etc. + + An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. + """ event: Literal["willDisappear"] # type: ignore[override] payload: dict[str, Any] From 0ce44d804082f51c71d58579ee2c78e90c612bec Mon Sep 17 00:00:00 2001 From: strohganoff Date: Wed, 2 Apr 2025 18:00:42 -0600 Subject: [PATCH 04/15] Fix up EventBase class to push code to get type hints of field into a class method --- streamdeck/models/events/base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/streamdeck/models/events/base.py b/streamdeck/models/events/base.py index fc32ccf..61dfaa9 100644 --- a/streamdeck/models/events/base.py +++ b/streamdeck/models/events/base.py @@ -37,16 +37,21 @@ def __init_subclass__(cls, **kwargs: Any) -> None: """Validate that the event field is a Literal[str] type.""" super().__init_subclass__(**kwargs) - model_event_type = get_type_hints(cls)["event"] + model_event_type = cls.get_event_type_annotations() if not is_literal_str_type(model_event_type): msg = f"The event field annotation must be a Literal[str] type. Given type: {model_event_type}" raise TypeError(msg) + @classmethod + def get_event_type_annotations(cls) -> type[object]: + """Get the type annotations of the subclass model's event field.""" + return get_type_hints(cls)["event"] + @classmethod def get_model_event_name(cls) -> tuple[str, ...]: """Get the value of the subclass model's event field Literal annotation.""" - model_event_type = get_type_hints(cls)["event"] + model_event_type = cls.get_event_type_annotations() # Ensure that the event field annotation is a Literal type. if not is_literal_str_type(model_event_type): From 9c8942c5d799a1b264d7e03a4778dc10667c5e38 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Thu, 3 Apr 2025 12:28:07 -0600 Subject: [PATCH 05/15] Create models for payload and subdata in the simple event models --- streamdeck/models/events/application.py | 12 +++- streamdeck/models/events/deep_link.py | 10 ++- streamdeck/models/events/devices.py | 65 ++++++++++++++++++- .../models/events/property_inspector.py | 3 +- streamdeck/models/events/title_parameters.py | 19 ++++-- 5 files changed, 94 insertions(+), 15 deletions(-) diff --git a/streamdeck/models/events/application.py b/streamdeck/models/events/application.py index 1f12c55..0144cf8 100644 --- a/streamdeck/models/events/application.py +++ b/streamdeck/models/events/application.py @@ -2,20 +2,26 @@ from typing import Literal +from pydantic import BaseModel + from streamdeck.models.events.base import EventBase -## EventBase implementation models of the Stream Deck Plugin SDK events. +class ApplicationPayload(BaseModel): + """Payload containing the name of the application that triggered the event.""" + application: str + """Name of the application that triggered the event.""" + class ApplicationDidLaunch(EventBase): """Occurs when a monitored application is launched.""" event: Literal["applicationDidLaunch"] # type: ignore[override] - payload: dict[Literal["application"], str] + payload: ApplicationPayload """Payload containing the name of the application that triggered the event.""" class ApplicationDidTerminate(EventBase): """Occurs when a monitored application terminates.""" event: Literal["applicationDidTerminate"] # type: ignore[override] - payload: dict[Literal["application"], str] + payload: ApplicationPayload """Payload containing the name of the application that triggered the event.""" diff --git a/streamdeck/models/events/deep_link.py b/streamdeck/models/events/deep_link.py index 5b847c5..bbaf321 100644 --- a/streamdeck/models/events/deep_link.py +++ b/streamdeck/models/events/deep_link.py @@ -1,8 +1,16 @@ from typing import Literal +from pydantic import BaseModel + from streamdeck.models.events.base import EventBase +class DeepLinkPayload(BaseModel): + """Payload containing information about the URL that triggered the event.""" + url: str + """The deep-link URL, with the prefix omitted.""" + + class DidReceiveDeepLink(EventBase): """Occurs when Stream Deck receives a deep-link message intended for the plugin. @@ -11,5 +19,5 @@ class DidReceiveDeepLink(EventBase): streamdeck://plugins/message//{MESSAGE}. """ event: Literal["didReceiveDeepLink"] # type: ignore[override] - payload: dict[Literal["url"], str] + payload: DeepLinkPayload """Payload containing information about the URL that triggered the event.""" diff --git a/streamdeck/models/events/devices.py b/streamdeck/models/events/devices.py index 83f224b..8b48fd2 100644 --- a/streamdeck/models/events/devices.py +++ b/streamdeck/models/events/devices.py @@ -1,16 +1,77 @@ -from typing import Any, Literal +from typing import Annotated, Final, Literal + +from pydantic import BaseModel, Field +from typing_extensions import TypedDict from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import DeviceSpecificEventMixin +DeviceTypeId = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + +DeviceType = Literal[ + "Stream Deck", + "Stream Deck Mini", + "Stream Deck XL", + "Stream Deck Mobile", + "Corsair GKeys", + "Stream Deck Pedal", + "Corsair Voyager", + "Stream Deck +", + "SCUF Controller", + "Stream Deck Neo", +] + + +DEVICE_TYPE_BY_ID: Final[dict[DeviceTypeId, DeviceType]] = { + 0: "Stream Deck", + 1: "Stream Deck Mini", + 2: "Stream Deck XL", + 3: "Stream Deck Mobile", + 4: "Corsair GKeys", + 5: "Stream Deck Pedal", + 6: "Corsair Voyager", + 7: "Stream Deck +", + 8: "SCUF Controller", + 9: "Stream Deck Neo", +} + + +class DeviceSize(TypedDict): + """Number of action slots, excluding dials / touchscreens, available to the device.""" + columns: int + rows: int + + +class DeviceInfo(BaseModel): + """Information about the newly connected device.""" + name: str + """Name of the device, as specified by the user in the Stream Deck application.""" + size: DeviceSize + """Number of action slots, excluding dials / touchscreens, available to the device.""" + _type: Annotated[DeviceTypeId, Field(alias="type")] + """The type (id) of the device that was connected.""" + + @property + def type(self) -> DeviceType: + """The type (product name) of the device that was connected.""" + if self._type not in DEVICE_TYPE_BY_ID: + msg = f"Unknown device type id: {self._type}" + raise ValueError(msg) + + return DEVICE_TYPE_BY_ID[self._type] + + class DeviceDidConnect(EventBase, DeviceSpecificEventMixin): """Occurs when a Stream Deck device is connected.""" event: Literal["deviceDidConnect"] # type: ignore[override] - deviceInfo: dict[str, Any] + deviceInfo: DeviceInfo """Information about the newly connected device.""" class DeviceDidDisconnect(EventBase, DeviceSpecificEventMixin): """Occurs when a Stream Deck device is disconnected.""" event: Literal["deviceDidDisconnect"] # type: ignore[override] + device: str + """Unique identifier of the Stream Deck device that this event is associated with.""" diff --git a/streamdeck/models/events/property_inspector.py b/streamdeck/models/events/property_inspector.py index 6838243..dd9839a 100644 --- a/streamdeck/models/events/property_inspector.py +++ b/streamdeck/models/events/property_inspector.py @@ -10,8 +10,7 @@ class DidReceivePropertyInspectorMessage(EventBase, ContextualEventMixin): """Occurs when a payload was received from the UI.""" event: Literal["sendToPlugin"] # type: ignore[override] payload: dict[str, Any] - - + """The data payload received from the UI.""" class PropertyInspectorDidAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the property inspector associated with the action becomes visible. diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index bcec6b6..d32aafe 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -1,32 +1,37 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Any, Literal, Optional +from pydantic import BaseModel from typing_extensions import TypedDict from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import DeviceSpecificEventMixin -class TitleParametersDict(TypedDict): +FontStyle = Literal["", "Bold Italic", "Bold", "Italic", "Regular"] +TitleAlignment = Literal["top", "middle", "bottom"] + + +class TitleParameters(BaseModel): """Defines aesthetic properties that determine how the title should be rendered.""" fontFamily: str """Font-family the title will be rendered with.""" fontSize: int """Font-size the title will be rendered in.""" - fontStyle: str + fontStyle: FontStyle """Typography of the title.""" fontUnderline: bool """Whether the font should be underlined.""" showTitle: bool """Whether the user has opted to show, or hide the title for this action instance.""" - titleAlignment: Literal["top", "middle", "bottom"] + titleAlignment: TitleAlignment """Alignment of the title.""" titleColor: str """Color of the title, represented as a hexadecimal value.""" -class TitleParametersDidChangePayload(TypedDict): +class TitleParametersDidChangePayload(BaseModel): """Contextualized information for this event.""" controller: Literal["Keypad", "Encoder"] """Defines the controller type the action is applicable to.""" @@ -34,11 +39,11 @@ class TitleParametersDidChangePayload(TypedDict): """Coordinates that identify the location of an action.""" settings: dict[str, Any] """Settings associated with the action instance.""" - state: int + state: Optional[int] # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" title: str """Title of the action, as specified by the user or dynamically by the plugin.""" - titleParameters: TitleParametersDict + titleParameters: TitleParameters """Defines aesthetic properties that determine how the title should be rendered.""" From de3a4478420ac5c9393b2a47869adefcfe211a78 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Thu, 3 Apr 2025 14:03:34 -0600 Subject: [PATCH 06/15] Flesh out models for payloads and subdata of the more complex event models --- streamdeck/models/events/common.py | 45 +++++++++++++++++++ streamdeck/models/events/devices.py | 2 - streamdeck/models/events/dials.py | 36 +++++++++++++-- streamdeck/models/events/keys.py | 43 ++++++++++++++++-- .../models/events/property_inspector.py | 3 +- streamdeck/models/events/settings.py | 25 +++++++++-- streamdeck/models/events/title_parameters.py | 9 ++-- streamdeck/models/events/touch_tap.py | 17 ++++++- streamdeck/models/events/visibility.py | 17 +++++-- 9 files changed, 174 insertions(+), 23 deletions(-) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index 4e5246f..19560da 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -1,5 +1,9 @@ from __future__ import annotations +from typing import Any, Literal, Optional + +from pydantic import BaseModel + ## Mixin classes for common event model fields. @@ -15,3 +19,44 @@ class DeviceSpecificEventMixin: """Mixin class for event models that have a device field.""" device: str """Unique identifier of the Stream Deck device that this event is associated with.""" + + +## Payload models used by multiple event models. + +class SingleActionPayload(BaseModel): + """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are not part of a multi-action.""" + controller: Literal["Encoder", "Keypad"] + """Defines the controller type the action is applicable to. + + 'Keypad' refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. + 'Encoder' refers to a dial / touchscreen. + """ + coordinates: dict[Literal["column", "row"], int] + """Coordinates that identify the location of the action instance on the device.""" + isInMultiAction: Literal[False] + """Indicates that this event is not part of a multi-action.""" + state: Optional[int] = None # noqa: UP007 + """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" + settings: dict[str, Any] + """Settings associated with the action instance.""" + + +class MultiActionPayload(BaseModel): + """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are part of a multi-action. + + NOTE: Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. + """ + controller: Literal["Keypad"] + """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. + + Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. + """ + isInMultiAction: Literal[True] + """Indicates that this event is part of a multi-action.""" + state: Optional[int] = None # noqa: UP007 + """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" + settings: dict[str, Any] + """Settings associated with the action instance.""" + + + diff --git a/streamdeck/models/events/devices.py b/streamdeck/models/events/devices.py index 8b48fd2..1ca3795 100644 --- a/streamdeck/models/events/devices.py +++ b/streamdeck/models/events/devices.py @@ -73,5 +73,3 @@ class DeviceDidConnect(EventBase, DeviceSpecificEventMixin): class DeviceDidDisconnect(EventBase, DeviceSpecificEventMixin): """Occurs when a Stream Deck device is disconnected.""" event: Literal["deviceDidDisconnect"] # type: ignore[override] - device: str - """Unique identifier of the Stream Deck device that this event is associated with.""" diff --git a/streamdeck/models/events/dials.py b/streamdeck/models/events/dials.py index 3d5ae36..b563f0c 100644 --- a/streamdeck/models/events/dials.py +++ b/streamdeck/models/events/dials.py @@ -2,26 +2,56 @@ from typing import Any, Literal +from pydantic import BaseModel + from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +## Payload models used in the below DialDown, DialRotate, and DialUp events + +class EncoderPayload(BaseModel): + """Contextualized information for a DialDown or DialUp event.""" + controller: Literal["Encoder"] + """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" + coordinates: dict[Literal["column", "row"], int] + """Coordinates that identify the location of the action instance on the device.""" + settings: dict[str, Any] + """Settings associated with the action instance.""" + + +class DialRotatePayload(BaseModel): + """Contextualized information for a DialRotate event.""" + controller: Literal["Encoder"] + """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" + coordinates: dict[Literal["column", "row"], int] + """Coordinates that identify the location of the action instance on the device.""" + pressed: bool + """Determines whether the dial was pressed whilst the rotation occurred.""" + ticks: int + """Number of ticks the dial was rotated; this can be a positive (clockwise) or negative (counter-clockwise) number.""" + settings: dict[str, Any] + """Settings associated with the action instance.""" + + +## Event models for DialDown, DialRotate, and DialUp events + class DialDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user presses a dial (Stream Deck +).""" event: Literal["dialDown"] # type: ignore[override] - payload: dict[str, Any] + payload: EncoderPayload """Contextualized information for this event.""" class DialRotate(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user rotates a dial (Stream Deck +).""" event: Literal["dialRotate"] # type: ignore[override] - payload: dict[str, Any] + payload: DialRotatePayload """Contextualized information for this event.""" class DialUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user releases a pressed dial (Stream Deck +).""" event: Literal["dialUp"] # type: ignore[override] - payload: dict[str, Any] + payload: EncoderPayload """Contextualized information for this event.""" diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index 8ad76ea..cacd5a1 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -1,20 +1,57 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Annotated, Any, Literal, Optional, Union + +from pydantic import BaseModel, Field from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +## Payload models used in the below KeyDown and KeyUp events + +class SingleActionKeyGesturePayload(BaseModel): + """Contextualized information for a KeyDown or KeyUp event that is not part of a multi-action.""" + controller: Optional[Literal["Keypad"]] = None # noqa: UP007 + """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" + coordinates: dict[Literal["column", "row"], int] + """Coordinates that identify the location of the action instance on the device.""" + isInMultiAction: Literal[False] + """Indicates that this event is not part of a multi-action.""" + state: Optional[int] = None # noqa: UP007 + """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" + settings: dict[str, Any] + """Settings associated with the action instance.""" + + +class MultiActionKeyGesturePayload(BaseModel): + """Contextualized information for a KeyDown or KeyUp event that is part of a multi-action.""" + controller: Optional[Literal["Keypad"]] = None # noqa: UP007 + """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" + isInMultiAction: Literal[True] + """Indicates that this event is part of a multi-action.""" + state: Optional[int] = None # noqa: UP007 + """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" + userDesiredState: int + """Desired state as specified by the user. + + Only applicable to actions that have multiple states defined within the manifest.json file, and when this action instance is part of a multi-action. + """ + settings: dict[str, Any] + """Settings associated with the action instance.""" + + +## Event models for KeyDown and KeyUp events + class KeyDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user presses a action down.""" event: Literal["keyDown"] # type: ignore[override] - payload: dict[str, Any] + payload: Annotated[Union[SingleActionKeyGesturePayload, MultiActionKeyGesturePayload], Field(discriminator="isInMultiAction")] # noqa: UP007 """Contextualized information for this event.""" class KeyUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user releases a pressed action.""" event: Literal["keyUp"] # type: ignore[override] - payload: dict[str, Any] + payload: Annotated[Union[SingleActionKeyGesturePayload, MultiActionKeyGesturePayload], Field(discriminator="isInMultiAction")] # noqa: UP007 """Contextualized information for this event.""" diff --git a/streamdeck/models/events/property_inspector.py b/streamdeck/models/events/property_inspector.py index dd9839a..9949568 100644 --- a/streamdeck/models/events/property_inspector.py +++ b/streamdeck/models/events/property_inspector.py @@ -7,11 +7,12 @@ class DidReceivePropertyInspectorMessage(EventBase, ContextualEventMixin): - """Occurs when a payload was received from the UI.""" + """Occurs when a message was received from the UI.""" event: Literal["sendToPlugin"] # type: ignore[override] payload: dict[str, Any] """The data payload received from the UI.""" + class PropertyInspectorDidAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the property inspector associated with the action becomes visible. diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py index 16fdb3a..b799434 100644 --- a/streamdeck/models/events/settings.py +++ b/streamdeck/models/events/settings.py @@ -1,18 +1,35 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Annotated, Any, Literal, Union + +from pydantic import BaseModel, Field from streamdeck.models.events.base import EventBase -from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +from streamdeck.models.events.common import ( + ContextualEventMixin, + DeviceSpecificEventMixin, + MultiActionPayload, + SingleActionPayload, +) class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the settings associated with an action instance are requested, or when the the settings were updated by the property inspector.""" event: Literal["didReceiveSettings"] # type: ignore[override] - payload: dict[str, Any] + payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="isInMultiAction")] # noqa: UP007 + """Contextualized information for this event.""" + + +## Models for didReceiveGlobalSettings event and its specific payload. + +class GlobalSettingsPayload(BaseModel): + """Additional information about the didReceiveGlobalSettings event that occurred.""" + settings: dict[str, Any] + """The global settings received from the Stream Deck.""" class DidReceiveGlobalSettings(EventBase): """Occurs when the plugin receives the global settings from the Stream Deck.""" event: Literal["didReceiveGlobalSettings"] # type: ignore[override] - payload: dict[Literal["settings"], dict[str, Any]] + payload: GlobalSettingsPayload + """Additional information about the event that occurred.""" diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index d32aafe..8959059 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -3,7 +3,6 @@ from typing import Any, Literal, Optional from pydantic import BaseModel -from typing_extensions import TypedDict from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import DeviceSpecificEventMixin @@ -37,14 +36,14 @@ class TitleParametersDidChangePayload(BaseModel): """Defines the controller type the action is applicable to.""" coordinates: dict[Literal["column", "row"], int] """Coordinates that identify the location of an action.""" - settings: dict[str, Any] - """Settings associated with the action instance.""" - state: Optional[int] # noqa: UP007 - """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" title: str """Title of the action, as specified by the user or dynamically by the plugin.""" titleParameters: TitleParameters """Defines aesthetic properties that determine how the title should be rendered.""" + state: Optional[int] # noqa: UP007 + """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" + settings: dict[str, Any] + """Settings associated with the action instance.""" class TitleParametersDidChange(EventBase, DeviceSpecificEventMixin): diff --git a/streamdeck/models/events/touch_tap.py b/streamdeck/models/events/touch_tap.py index 23dac66..2a29f8a 100644 --- a/streamdeck/models/events/touch_tap.py +++ b/streamdeck/models/events/touch_tap.py @@ -2,12 +2,27 @@ from typing import Any, Literal +from pydantic import BaseModel + from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +class TouchTapPayload(BaseModel): + """Contextualized information for a TouchTap event.""" + controller: Literal["Encoder"] + """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" + coordinates: dict[Literal["column", "row"], int] + """Coordinates that identify the location of the action instance on the device.""" + hold: bool + """Determines whether the tap was considered 'held'.""" + tapPos: tuple[int, int] # noqa: N815 + """Coordinates of where the touchscreen tap occurred, relative to the canvas of the action.""" + settings: dict[str, Any] + """Settings associated with the action instance.""" + class TouchTap(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user taps the touchscreen (Stream Deck +).""" event: Literal["touchTap"] # type: ignore[override] - payload: dict[str, Any] + payload: TouchTapPayload """Contextualized information for this event.""" diff --git a/streamdeck/models/events/visibility.py b/streamdeck/models/events/visibility.py index 1dcdc0b..bf7f6a3 100644 --- a/streamdeck/models/events/visibility.py +++ b/streamdeck/models/events/visibility.py @@ -1,10 +1,19 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Annotated, Literal, Union + +from pydantic import Field from streamdeck.models.events.base import EventBase -from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +from streamdeck.models.events.common import ( + ContextualEventMixin, + DeviceSpecificEventMixin, + MultiActionPayload, + SingleActionPayload, +) + +## Event models for WillAppear and WillDisappear events class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when an action appears on the Stream Deck due to the user navigating to another page, profile, folder, etc. @@ -13,7 +22,7 @@ class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. """ event: Literal["willAppear"] # type: ignore[override] - payload: dict[str, Any] + payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="isInMultiAction")] # noqa: UP007 class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): @@ -22,4 +31,4 @@ class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. """ event: Literal["willDisappear"] # type: ignore[override] - payload: dict[str, Any] + payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="isInMultiAction")] # noqa: UP007 From 85421bbf748c2a26dad769e148ca1b2175ede9a6 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Fri, 4 Apr 2025 13:48:17 -0600 Subject: [PATCH 07/15] Minor improvements to models and field aliases --- pyproject.toml | 4 +-- streamdeck/models/events/adapter.py | 1 - streamdeck/models/events/base.py | 4 +-- streamdeck/models/events/common.py | 2 +- streamdeck/models/events/devices.py | 33 ++++++++++++++------ streamdeck/models/events/keys.py | 2 +- streamdeck/models/events/title_parameters.py | 20 ++++++------ streamdeck/models/events/touch_tap.py | 6 ++-- 8 files changed, 43 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f5cafd1..fef294a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,8 +58,8 @@ "dependencies" = [ # "jmespath >= 1.0.1", # Upcoming pattern-filtering functionality "platformdirs >= 4.3.6", - "pydantic >= 2.9.2", - "pydantic_core >= 2.23.4", + "pydantic >= 2.11", + "pydantic_core >= 2.33", "tomli >= 2.0.2", "websockets >= 13.1", "typer >= 0.15.1", diff --git a/streamdeck/models/events/adapter.py b/streamdeck/models/events/adapter.py index d8e135d..347fa15 100644 --- a/streamdeck/models/events/adapter.py +++ b/streamdeck/models/events/adapter.py @@ -11,7 +11,6 @@ from streamdeck.models.events.base import EventBase -## EventAdapter class for handling and extending available event models. class EventAdapter: """TypeAdapter-encompassing class for handling and extending available event models.""" diff --git a/streamdeck/models/events/base.py b/streamdeck/models/events/base.py index 61dfaa9..49e2ea5 100644 --- a/streamdeck/models/events/base.py +++ b/streamdeck/models/events/base.py @@ -20,12 +20,12 @@ def is_literal_str_type(value: object | None) -> TypeIs[LiteralString]: return all(isinstance(literal_value, str) for literal_value in get_args(value)) -## EventBase implementation models of the Stream Deck Plugin SDK events. +## EventBase implementation model of the Stream Deck Plugin SDK events. class EventBase(BaseModel, ABC): """Base class for event models that represent Stream Deck Plugin SDK events.""" # Configure to use the docstrings of the fields as the field descriptions. - model_config = ConfigDict(use_attribute_docstrings=True) + model_config = ConfigDict(use_attribute_docstrings=True, serialize_by_alias=True) event: str """Name of the event used to identify what occurred. diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index 19560da..37f6fbf 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -21,7 +21,7 @@ class DeviceSpecificEventMixin: """Unique identifier of the Stream Deck device that this event is associated with.""" -## Payload models used by multiple event models. +## Payload models and metadata used by multiple event models. class SingleActionPayload(BaseModel): """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are not part of a multi-action.""" diff --git a/streamdeck/models/events/devices.py b/streamdeck/models/events/devices.py index 1ca3795..1535450 100644 --- a/streamdeck/models/events/devices.py +++ b/streamdeck/models/events/devices.py @@ -1,4 +1,4 @@ -from typing import Annotated, Final, Literal +from typing import Annotated, Final, Literal, NamedTuple from pydantic import BaseModel, Field from typing_extensions import TypedDict @@ -38,7 +38,13 @@ } -class DeviceSize(TypedDict): +class DeviceSizeDict(TypedDict): + """Number of action slots, excluding dials / touchscreens, available to the device.""" + columns: int + rows: int + + +class DeviceSize(NamedTuple): """Number of action slots, excluding dials / touchscreens, available to the device.""" columns: int rows: int @@ -48,25 +54,34 @@ class DeviceInfo(BaseModel): """Information about the newly connected device.""" name: str """Name of the device, as specified by the user in the Stream Deck application.""" - size: DeviceSize - """Number of action slots, excluding dials / touchscreens, available to the device.""" - _type: Annotated[DeviceTypeId, Field(alias="type")] + type_id: Annotated[DeviceTypeId, Field(alias="type", repr=False)] """The type (id) of the device that was connected.""" + size_obj: Annotated[DeviceSizeDict, Field(alias="size", repr=False)] + """Number of action slots, excluding dials / touchscreens, available to the device.""" @property def type(self) -> DeviceType: """The type (product name) of the device that was connected.""" - if self._type not in DEVICE_TYPE_BY_ID: - msg = f"Unknown device type id: {self._type}" + if self.type_id not in DEVICE_TYPE_BY_ID: + msg = f"Unknown device type id: {self.type_id}" raise ValueError(msg) - return DEVICE_TYPE_BY_ID[self._type] + return DEVICE_TYPE_BY_ID[self.type_id] + + @property + def size(self) -> DeviceSize: + """Number of action slots, excluding dials / touchscreens, available to the device.""" + return DeviceSize(**self.size_obj) + + def __repr__(self) -> str: + """Return a string representation of the device info.""" + return f"DeviceInfo(name={self.name}, type={self.type}, size={self.size})" class DeviceDidConnect(EventBase, DeviceSpecificEventMixin): """Occurs when a Stream Deck device is connected.""" event: Literal["deviceDidConnect"] # type: ignore[override] - deviceInfo: DeviceInfo + device_info: Annotated[DeviceInfo, Field(alias="deviceInfo")] """Information about the newly connected device.""" diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index cacd5a1..b6575a7 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -32,7 +32,7 @@ class MultiActionKeyGesturePayload(BaseModel): """Indicates that this event is part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - userDesiredState: int + user_desired_state: Annotated[int, Field(alias="userDesiredState")] """Desired state as specified by the user. Only applicable to actions that have multiple states defined within the manifest.json file, and when this action instance is part of a multi-action. diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index 8959059..323adb9 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Any, Literal, Optional +from typing import Annotated, Literal, Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import DeviceSpecificEventMixin @@ -14,19 +14,19 @@ class TitleParameters(BaseModel): """Defines aesthetic properties that determine how the title should be rendered.""" - fontFamily: str + font_family: Annotated[str, Field(alias="fontFamily")] """Font-family the title will be rendered with.""" - fontSize: int + font_size: Annotated[int, Field(alias="fontSize")] """Font-size the title will be rendered in.""" - fontStyle: FontStyle + font_style: Annotated[FontStyle, Field(alias="fontStyle")] """Typography of the title.""" - fontUnderline: bool + font_underline: Annotated[bool, Field(alias="fontUnderline")] """Whether the font should be underlined.""" - showTitle: bool + show_title: Annotated[bool, Field(alias="showTitle")] """Whether the user has opted to show, or hide the title for this action instance.""" - titleAlignment: TitleAlignment + title_alignment: Annotated[str, Field(alias="titleAlignment")] """Alignment of the title.""" - titleColor: str + title_color: Annotated[str, Field(alias="titleColor")] """Color of the title, represented as a hexadecimal value.""" @@ -38,7 +38,7 @@ class TitleParametersDidChangePayload(BaseModel): """Coordinates that identify the location of an action.""" title: str """Title of the action, as specified by the user or dynamically by the plugin.""" - titleParameters: TitleParameters + title_parameters: Annotated[TitleParameters, Field(alias="titleParameters")] """Defines aesthetic properties that determine how the title should be rendered.""" state: Optional[int] # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" diff --git a/streamdeck/models/events/touch_tap.py b/streamdeck/models/events/touch_tap.py index 2a29f8a..e2f62e8 100644 --- a/streamdeck/models/events/touch_tap.py +++ b/streamdeck/models/events/touch_tap.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Annotated, Literal -from pydantic import BaseModel +from pydantic import Field from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin @@ -16,7 +16,7 @@ class TouchTapPayload(BaseModel): """Coordinates that identify the location of the action instance on the device.""" hold: bool """Determines whether the tap was considered 'held'.""" - tapPos: tuple[int, int] # noqa: N815 + tap_position: Annotated[tuple[int, int], Field(alias="tapPos")] """Coordinates of where the touchscreen tap occurred, relative to the canvas of the action.""" settings: dict[str, Any] """Settings associated with the action instance.""" From bc29041571ce3d7e2b4f522c3dda67c7e7b9f7e3 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Fri, 4 Apr 2025 14:05:50 -0600 Subject: [PATCH 08/15] Replace dictionary values of arbitrary data with TypeAlias --- streamdeck/models/events/common.py | 8 ++++++-- streamdeck/models/events/dials.py | 9 ++++----- streamdeck/models/events/keys.py | 9 ++++----- streamdeck/models/events/property_inspector.py | 10 +++++++--- streamdeck/models/events/settings.py | 5 +++-- streamdeck/models/events/title_parameters.py | 7 +++---- streamdeck/models/events/touch_tap.py | 9 ++++++--- 7 files changed, 33 insertions(+), 24 deletions(-) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index 37f6fbf..76ab88b 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -23,6 +23,10 @@ class DeviceSpecificEventMixin: ## Payload models and metadata used by multiple event models. +PluginDefinedData = dict[str, Any] +"""Data of arbitrary structure that is defined in and relevant to the plugin.""" + + class SingleActionPayload(BaseModel): """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are not part of a multi-action.""" controller: Literal["Encoder", "Keypad"] @@ -37,7 +41,7 @@ class SingleActionPayload(BaseModel): """Indicates that this event is not part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - settings: dict[str, Any] + settings: PluginDefinedData """Settings associated with the action instance.""" @@ -55,7 +59,7 @@ class MultiActionPayload(BaseModel): """Indicates that this event is part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - settings: dict[str, Any] + settings: PluginDefinedData """Settings associated with the action instance.""" diff --git a/streamdeck/models/events/dials.py b/streamdeck/models/events/dials.py index b563f0c..0da003c 100644 --- a/streamdeck/models/events/dials.py +++ b/streamdeck/models/events/dials.py @@ -1,11 +1,11 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Literal from pydantic import BaseModel from streamdeck.models.events.base import EventBase -from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin, PluginDefinedData ## Payload models used in the below DialDown, DialRotate, and DialUp events @@ -16,8 +16,7 @@ class EncoderPayload(BaseModel): """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" coordinates: dict[Literal["column", "row"], int] """Coordinates that identify the location of the action instance on the device.""" - settings: dict[str, Any] - """Settings associated with the action instance.""" + settings: PluginDefinedData class DialRotatePayload(BaseModel): @@ -30,7 +29,7 @@ class DialRotatePayload(BaseModel): """Determines whether the dial was pressed whilst the rotation occurred.""" ticks: int """Number of ticks the dial was rotated; this can be a positive (clockwise) or negative (counter-clockwise) number.""" - settings: dict[str, Any] + settings: PluginDefinedData """Settings associated with the action instance.""" diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index b6575a7..8511b25 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -1,11 +1,11 @@ from __future__ import annotations -from typing import Annotated, Any, Literal, Optional, Union +from typing import Annotated, Literal, Optional, Union from pydantic import BaseModel, Field from streamdeck.models.events.base import EventBase -from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin, PluginDefinedData ## Payload models used in the below KeyDown and KeyUp events @@ -20,7 +20,7 @@ class SingleActionKeyGesturePayload(BaseModel): """Indicates that this event is not part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - settings: dict[str, Any] + settings: PluginDefinedData """Settings associated with the action instance.""" @@ -37,8 +37,7 @@ class MultiActionKeyGesturePayload(BaseModel): Only applicable to actions that have multiple states defined within the manifest.json file, and when this action instance is part of a multi-action. """ - settings: dict[str, Any] - """Settings associated with the action instance.""" + settings: PluginDefinedData ## Event models for KeyDown and KeyUp events diff --git a/streamdeck/models/events/property_inspector.py b/streamdeck/models/events/property_inspector.py index 9949568..8097fd4 100644 --- a/streamdeck/models/events/property_inspector.py +++ b/streamdeck/models/events/property_inspector.py @@ -1,15 +1,19 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Literal from streamdeck.models.events.base import EventBase -from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +from streamdeck.models.events.common import ( + ContextualEventMixin, + DeviceSpecificEventMixin, + PluginDefinedData, +) class DidReceivePropertyInspectorMessage(EventBase, ContextualEventMixin): """Occurs when a message was received from the UI.""" event: Literal["sendToPlugin"] # type: ignore[override] - payload: dict[str, Any] + payload: PluginDefinedData """The data payload received from the UI.""" diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py index b799434..82e6584 100644 --- a/streamdeck/models/events/settings.py +++ b/streamdeck/models/events/settings.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Annotated, Any, Literal, Union +from typing import Annotated, Literal, Union from pydantic import BaseModel, Field @@ -9,6 +9,7 @@ ContextualEventMixin, DeviceSpecificEventMixin, MultiActionPayload, + PluginDefinedData, SingleActionPayload, ) @@ -24,7 +25,7 @@ class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMix class GlobalSettingsPayload(BaseModel): """Additional information about the didReceiveGlobalSettings event that occurred.""" - settings: dict[str, Any] + settings: PluginDefinedData """The global settings received from the Stream Deck.""" diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index 323adb9..0ee2084 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field from streamdeck.models.events.base import EventBase -from streamdeck.models.events.common import DeviceSpecificEventMixin +from streamdeck.models.events.common import DeviceSpecificEventMixin, PluginDefinedData + FontStyle = Literal["", "Bold Italic", "Bold", "Italic", "Regular"] @@ -42,9 +43,7 @@ class TitleParametersDidChangePayload(BaseModel): """Defines aesthetic properties that determine how the title should be rendered.""" state: Optional[int] # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - settings: dict[str, Any] - """Settings associated with the action instance.""" - + settings: PluginDefinedData class TitleParametersDidChange(EventBase, DeviceSpecificEventMixin): """Occurs when the user updates an action's title settings in the Stream Deck application.""" diff --git a/streamdeck/models/events/touch_tap.py b/streamdeck/models/events/touch_tap.py index e2f62e8..3b2b928 100644 --- a/streamdeck/models/events/touch_tap.py +++ b/streamdeck/models/events/touch_tap.py @@ -5,7 +5,11 @@ from pydantic import Field from streamdeck.models.events.base import EventBase -from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin +from streamdeck.models.events.common import ( + ContextualEventMixin, + DeviceSpecificEventMixin, + PluginDefinedData, +) class TouchTapPayload(BaseModel): @@ -18,8 +22,7 @@ class TouchTapPayload(BaseModel): """Determines whether the tap was considered 'held'.""" tap_position: Annotated[tuple[int, int], Field(alias="tapPos")] """Coordinates of where the touchscreen tap occurred, relative to the canvas of the action.""" - settings: dict[str, Any] - """Settings associated with the action instance.""" + settings: PluginDefinedData class TouchTap(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user taps the touchscreen (Stream Deck +).""" From 40d2cee33b90016e5d9a4d32b5a4f2f0fa2f41a8 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Fri, 4 Apr 2025 14:26:45 -0600 Subject: [PATCH 09/15] Create new base model with shared configurations for event & payload models --- streamdeck/models/events/application.py | 6 ++-- streamdeck/models/events/base.py | 30 ++++++++++++++++++-- streamdeck/models/events/common.py | 6 ++-- streamdeck/models/events/deep_link.py | 6 ++-- streamdeck/models/events/devices.py | 6 ++-- streamdeck/models/events/dials.py | 8 ++---- streamdeck/models/events/keys.py | 8 +++--- streamdeck/models/events/settings.py | 6 ++-- streamdeck/models/events/title_parameters.py | 8 +++--- streamdeck/models/events/touch_tap.py | 2 +- 10 files changed, 53 insertions(+), 33 deletions(-) diff --git a/streamdeck/models/events/application.py b/streamdeck/models/events/application.py index 0144cf8..6e4a788 100644 --- a/streamdeck/models/events/application.py +++ b/streamdeck/models/events/application.py @@ -2,12 +2,10 @@ from typing import Literal -from pydantic import BaseModel +from streamdeck.models.events.base import ConfiguredBaseModel, EventBase -from streamdeck.models.events.base import EventBase - -class ApplicationPayload(BaseModel): +class ApplicationPayload(ConfiguredBaseModel): """Payload containing the name of the application that triggered the event.""" application: str """Name of the application that triggered the event.""" diff --git a/streamdeck/models/events/base.py b/streamdeck/models/events/base.py index 49e2ea5..b003a76 100644 --- a/streamdeck/models/events/base.py +++ b/streamdeck/models/events/base.py @@ -4,7 +4,33 @@ from typing import Any, Literal, get_args, get_type_hints from pydantic import BaseModel, ConfigDict -from typing_extensions import LiteralString, TypeIs # noqa: UP035 +from typing_extensions import ( # noqa: UP035 + LiteralString, + TypeIs, + override, +) + + +class ConfiguredBaseModel(BaseModel, ABC): + """Base class for models that share the same configuration.""" + # Configure to use the docstrings of the fields as the field descriptions, + # and to serialize the fields by their aliases. + model_config = ConfigDict(use_attribute_docstrings=True, serialize_by_alias=True) + + @override + def model_dump_json(self, **kwargs: Any) -> str: + """Dump the model to JSON, excluding default values by default. + + Fields with default values in this context are those that are not required by the model, + and are given a default value of None. Thus, for the serialized JSON to have parity with the + StreamDeck-provided JSON event messages, we need to exclude fields not found in the event message. + + Unfortunately, the `exclude_defaults` option is not available in the ConfigDict configuration, + nor in the Field parameters. To work around this, we wrap the `model_dump_json` method + to set `exclude_defaults` to `True` by default. + """ + kwargs.setdefault("exclude_defaults", True) + return super().model_dump_json(**kwargs) def is_literal_str_type(value: object | None) -> TypeIs[LiteralString]: @@ -22,7 +48,7 @@ def is_literal_str_type(value: object | None) -> TypeIs[LiteralString]: ## EventBase implementation model of the Stream Deck Plugin SDK events. -class EventBase(BaseModel, ABC): +class EventBase(ConfiguredBaseModel, ABC): """Base class for event models that represent Stream Deck Plugin SDK events.""" # Configure to use the docstrings of the fields as the field descriptions. model_config = ConfigDict(use_attribute_docstrings=True, serialize_by_alias=True) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index 76ab88b..b404532 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -2,7 +2,7 @@ from typing import Any, Literal, Optional -from pydantic import BaseModel +from streamdeck.models.events.base import ConfiguredBaseModel ## Mixin classes for common event model fields. @@ -27,7 +27,7 @@ class DeviceSpecificEventMixin: """Data of arbitrary structure that is defined in and relevant to the plugin.""" -class SingleActionPayload(BaseModel): +class SingleActionPayload(ConfiguredBaseModel): """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are not part of a multi-action.""" controller: Literal["Encoder", "Keypad"] """Defines the controller type the action is applicable to. @@ -45,7 +45,7 @@ class SingleActionPayload(BaseModel): """Settings associated with the action instance.""" -class MultiActionPayload(BaseModel): +class MultiActionPayload(ConfiguredBaseModel): """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are part of a multi-action. NOTE: Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. diff --git a/streamdeck/models/events/deep_link.py b/streamdeck/models/events/deep_link.py index bbaf321..6331ce5 100644 --- a/streamdeck/models/events/deep_link.py +++ b/streamdeck/models/events/deep_link.py @@ -1,11 +1,9 @@ from typing import Literal -from pydantic import BaseModel +from streamdeck.models.events.base import ConfiguredBaseModel, EventBase -from streamdeck.models.events.base import EventBase - -class DeepLinkPayload(BaseModel): +class DeepLinkPayload(ConfiguredBaseModel): """Payload containing information about the URL that triggered the event.""" url: str """The deep-link URL, with the prefix omitted.""" diff --git a/streamdeck/models/events/devices.py b/streamdeck/models/events/devices.py index 1535450..50c1ac9 100644 --- a/streamdeck/models/events/devices.py +++ b/streamdeck/models/events/devices.py @@ -1,9 +1,9 @@ from typing import Annotated, Final, Literal, NamedTuple -from pydantic import BaseModel, Field +from pydantic import Field from typing_extensions import TypedDict -from streamdeck.models.events.base import EventBase +from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import DeviceSpecificEventMixin @@ -50,7 +50,7 @@ class DeviceSize(NamedTuple): rows: int -class DeviceInfo(BaseModel): +class DeviceInfo(ConfiguredBaseModel): """Information about the newly connected device.""" name: str """Name of the device, as specified by the user in the Stream Deck application.""" diff --git a/streamdeck/models/events/dials.py b/streamdeck/models/events/dials.py index 0da003c..944b8ed 100644 --- a/streamdeck/models/events/dials.py +++ b/streamdeck/models/events/dials.py @@ -2,15 +2,13 @@ from typing import Literal -from pydantic import BaseModel - -from streamdeck.models.events.base import EventBase +from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin, PluginDefinedData ## Payload models used in the below DialDown, DialRotate, and DialUp events -class EncoderPayload(BaseModel): +class EncoderPayload(ConfiguredBaseModel): """Contextualized information for a DialDown or DialUp event.""" controller: Literal["Encoder"] """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" @@ -19,7 +17,7 @@ class EncoderPayload(BaseModel): settings: PluginDefinedData -class DialRotatePayload(BaseModel): +class DialRotatePayload(ConfiguredBaseModel): """Contextualized information for a DialRotate event.""" controller: Literal["Encoder"] """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index 8511b25..407e37f 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -2,15 +2,15 @@ from typing import Annotated, Literal, Optional, Union -from pydantic import BaseModel, Field +from pydantic import Field -from streamdeck.models.events.base import EventBase +from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin, PluginDefinedData ## Payload models used in the below KeyDown and KeyUp events -class SingleActionKeyGesturePayload(BaseModel): +class SingleActionKeyGesturePayload(ConfiguredBaseModel): """Contextualized information for a KeyDown or KeyUp event that is not part of a multi-action.""" controller: Optional[Literal["Keypad"]] = None # noqa: UP007 """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" @@ -24,7 +24,7 @@ class SingleActionKeyGesturePayload(BaseModel): """Settings associated with the action instance.""" -class MultiActionKeyGesturePayload(BaseModel): +class MultiActionKeyGesturePayload(ConfiguredBaseModel): """Contextualized information for a KeyDown or KeyUp event that is part of a multi-action.""" controller: Optional[Literal["Keypad"]] = None # noqa: UP007 """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py index 82e6584..e472c82 100644 --- a/streamdeck/models/events/settings.py +++ b/streamdeck/models/events/settings.py @@ -2,9 +2,9 @@ from typing import Annotated, Literal, Union -from pydantic import BaseModel, Field +from pydantic import Field -from streamdeck.models.events.base import EventBase +from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ( ContextualEventMixin, DeviceSpecificEventMixin, @@ -23,7 +23,7 @@ class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMix ## Models for didReceiveGlobalSettings event and its specific payload. -class GlobalSettingsPayload(BaseModel): +class GlobalSettingsPayload(ConfiguredBaseModel): """Additional information about the didReceiveGlobalSettings event that occurred.""" settings: PluginDefinedData """The global settings received from the Stream Deck.""" diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index 0ee2084..2862f14 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -2,9 +2,9 @@ from typing import Annotated, Literal, Optional -from pydantic import BaseModel, Field +from pydantic import Field -from streamdeck.models.events.base import EventBase +from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import DeviceSpecificEventMixin, PluginDefinedData @@ -13,7 +13,7 @@ TitleAlignment = Literal["top", "middle", "bottom"] -class TitleParameters(BaseModel): +class TitleParameters(ConfiguredBaseModel): """Defines aesthetic properties that determine how the title should be rendered.""" font_family: Annotated[str, Field(alias="fontFamily")] """Font-family the title will be rendered with.""" @@ -31,7 +31,7 @@ class TitleParameters(BaseModel): """Color of the title, represented as a hexadecimal value.""" -class TitleParametersDidChangePayload(BaseModel): +class TitleParametersDidChangePayload(ConfiguredBaseModel): """Contextualized information for this event.""" controller: Literal["Keypad", "Encoder"] """Defines the controller type the action is applicable to.""" diff --git a/streamdeck/models/events/touch_tap.py b/streamdeck/models/events/touch_tap.py index 3b2b928..f02a5a7 100644 --- a/streamdeck/models/events/touch_tap.py +++ b/streamdeck/models/events/touch_tap.py @@ -12,7 +12,7 @@ ) -class TouchTapPayload(BaseModel): +class TouchTapPayload(ConfiguredBaseModel): """Contextualized information for a TouchTap event.""" controller: Literal["Encoder"] """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" From 630266da160ddf4213114ba141fb89c4bd6f7712 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Fri, 4 Apr 2025 14:36:55 -0600 Subject: [PATCH 10/15] Add alias for isInMultiAction field, and handle discriminator appropriately --- streamdeck/models/events/keys.py | 6 +++--- streamdeck/models/events/settings.py | 2 +- streamdeck/models/events/visibility.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index 407e37f..a8893d1 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -16,7 +16,7 @@ class SingleActionKeyGesturePayload(ConfiguredBaseModel): """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" coordinates: dict[Literal["column", "row"], int] """Coordinates that identify the location of the action instance on the device.""" - isInMultiAction: Literal[False] + is_in_multi_action: Annotated[Literal[False], Field(alias="isInMultiAction")] """Indicates that this event is not part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" @@ -28,7 +28,7 @@ class MultiActionKeyGesturePayload(ConfiguredBaseModel): """Contextualized information for a KeyDown or KeyUp event that is part of a multi-action.""" controller: Optional[Literal["Keypad"]] = None # noqa: UP007 """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" - isInMultiAction: Literal[True] + is_in_multi_action: Annotated[Literal[True], Field(alias="isInMultiAction")] """Indicates that this event is part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" @@ -52,5 +52,5 @@ class KeyDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): class KeyUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user releases a pressed action.""" event: Literal["keyUp"] # type: ignore[override] - payload: Annotated[Union[SingleActionKeyGesturePayload, MultiActionKeyGesturePayload], Field(discriminator="isInMultiAction")] # noqa: UP007 + payload: Annotated[Union[SingleActionKeyGesturePayload, MultiActionKeyGesturePayload], Field(discriminator="is_in_multi_action")] # noqa: UP007 """Contextualized information for this event.""" diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py index e472c82..9025a85 100644 --- a/streamdeck/models/events/settings.py +++ b/streamdeck/models/events/settings.py @@ -17,7 +17,7 @@ class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the settings associated with an action instance are requested, or when the the settings were updated by the property inspector.""" event: Literal["didReceiveSettings"] # type: ignore[override] - payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="isInMultiAction")] # noqa: UP007 + payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="is_in_multi_action")] # noqa: UP007 """Contextualized information for this event.""" diff --git a/streamdeck/models/events/visibility.py b/streamdeck/models/events/visibility.py index bf7f6a3..09bc396 100644 --- a/streamdeck/models/events/visibility.py +++ b/streamdeck/models/events/visibility.py @@ -22,7 +22,7 @@ class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. """ event: Literal["willAppear"] # type: ignore[override] - payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="isInMultiAction")] # noqa: UP007 + payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="is_in_multi_action")] # noqa: UP007 class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): @@ -31,4 +31,4 @@ class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. """ event: Literal["willDisappear"] # type: ignore[override] - payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="isInMultiAction")] # noqa: UP007 + payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="is_in_multi_action")] # noqa: UP007 From 0d38930fe9fa66662b72938e73bed64eb013ff61 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Fri, 4 Apr 2025 15:02:55 -0600 Subject: [PATCH 11/15] Create Mixin class for payload models that have the field; make the field data represented as a NamedTuple --- streamdeck/models/events/common.py | 43 ++++++++++++++++++-- streamdeck/models/events/dials.py | 15 +++---- streamdeck/models/events/keys.py | 11 +++-- streamdeck/models/events/title_parameters.py | 11 ++--- streamdeck/models/events/touch_tap.py | 5 +-- 5 files changed, 63 insertions(+), 22 deletions(-) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index b404532..d67e238 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -27,7 +27,46 @@ class DeviceSpecificEventMixin: """Data of arbitrary structure that is defined in and relevant to the plugin.""" -class SingleActionPayload(ConfiguredBaseModel): +class CoordinatesDict(TypedDict): + """Coordinates that identify the location of an action.""" + + column: int + """Column the action instance is located in, indexed from 0.""" + row: int + """Row the action instance is located on, indexed from 0. + + When the device is DeviceType.StreamDeckPlus the row can be 0 for keys (Keypad), + and will always be 0 for dials (Encoder). + """ + + +class Coordinates(NamedTuple): + """Coordinates that identify the location of an action.""" + + column: int + """Column the action instance is located in, indexed from 0.""" + row: int + """Row the action instance is located on, indexed from 0. + + When the device is DeviceType.StreamDeckPlus the row can be 0 for keys (Keypad), + and will always be 0 for dials (Encoder). + """ + + +class CoordinatesPayloadMixin: + """Mixin class for event models that have a coordinates field.""" + coordinates_obj: Annotated[ + CoordinatesDict, Field(alias="coordinates", repr=False) + ] + """Coordinates dictionary that identify the location of the action instance on the device.""" + + @property + def coordinates(self) -> Coordinates: + """Coordinates that identify the location of the action instance on the device.""" + return Coordinates(**self.coordinates_obj) + + +class SingleActionPayload(ConfiguredBaseModel, CoordinatesPayloadMixin): """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are not part of a multi-action.""" controller: Literal["Encoder", "Keypad"] """Defines the controller type the action is applicable to. @@ -35,8 +74,6 @@ class SingleActionPayload(ConfiguredBaseModel): 'Keypad' refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. 'Encoder' refers to a dial / touchscreen. """ - coordinates: dict[Literal["column", "row"], int] - """Coordinates that identify the location of the action instance on the device.""" isInMultiAction: Literal[False] """Indicates that this event is not part of a multi-action.""" state: Optional[int] = None # noqa: UP007 diff --git a/streamdeck/models/events/dials.py b/streamdeck/models/events/dials.py index 944b8ed..f87f09c 100644 --- a/streamdeck/models/events/dials.py +++ b/streamdeck/models/events/dials.py @@ -3,26 +3,27 @@ from typing import Literal from streamdeck.models.events.base import ConfiguredBaseModel, EventBase -from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin, PluginDefinedData +from streamdeck.models.events.common import ( + ContextualEventMixin, + CoordinatesPayloadMixin, + DeviceSpecificEventMixin, + PluginDefinedData, +) ## Payload models used in the below DialDown, DialRotate, and DialUp events -class EncoderPayload(ConfiguredBaseModel): +class EncoderPayload(ConfiguredBaseModel, CoordinatesPayloadMixin): """Contextualized information for a DialDown or DialUp event.""" controller: Literal["Encoder"] """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" - coordinates: dict[Literal["column", "row"], int] - """Coordinates that identify the location of the action instance on the device.""" settings: PluginDefinedData -class DialRotatePayload(ConfiguredBaseModel): +class DialRotatePayload(ConfiguredBaseModel, CoordinatesPayloadMixin): """Contextualized information for a DialRotate event.""" controller: Literal["Encoder"] """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" - coordinates: dict[Literal["column", "row"], int] - """Coordinates that identify the location of the action instance on the device.""" pressed: bool """Determines whether the dial was pressed whilst the rotation occurred.""" ticks: int diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index a8893d1..d866d50 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -5,17 +5,20 @@ from pydantic import Field from streamdeck.models.events.base import ConfiguredBaseModel, EventBase -from streamdeck.models.events.common import ContextualEventMixin, DeviceSpecificEventMixin, PluginDefinedData +from streamdeck.models.events.common import ( + ContextualEventMixin, + CoordinatesPayloadMixin, + DeviceSpecificEventMixin, + PluginDefinedData, +) ## Payload models used in the below KeyDown and KeyUp events -class SingleActionKeyGesturePayload(ConfiguredBaseModel): +class SingleActionKeyGesturePayload(ConfiguredBaseModel, CoordinatesPayloadMixin): """Contextualized information for a KeyDown or KeyUp event that is not part of a multi-action.""" controller: Optional[Literal["Keypad"]] = None # noqa: UP007 """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" - coordinates: dict[Literal["column", "row"], int] - """Coordinates that identify the location of the action instance on the device.""" is_in_multi_action: Annotated[Literal[False], Field(alias="isInMultiAction")] """Indicates that this event is not part of a multi-action.""" state: Optional[int] = None # noqa: UP007 diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index 2862f14..62b207c 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -5,8 +5,11 @@ from pydantic import Field from streamdeck.models.events.base import ConfiguredBaseModel, EventBase -from streamdeck.models.events.common import DeviceSpecificEventMixin, PluginDefinedData - +from streamdeck.models.events.common import ( + CoordinatesPayloadMixin, + DeviceSpecificEventMixin, + PluginDefinedData, +) FontStyle = Literal["", "Bold Italic", "Bold", "Italic", "Regular"] @@ -31,12 +34,10 @@ class TitleParameters(ConfiguredBaseModel): """Color of the title, represented as a hexadecimal value.""" -class TitleParametersDidChangePayload(ConfiguredBaseModel): +class TitleParametersDidChangePayload(ConfiguredBaseModel, CoordinatesPayloadMixin): """Contextualized information for this event.""" controller: Literal["Keypad", "Encoder"] """Defines the controller type the action is applicable to.""" - coordinates: dict[Literal["column", "row"], int] - """Coordinates that identify the location of an action.""" title: str """Title of the action, as specified by the user or dynamically by the plugin.""" title_parameters: Annotated[TitleParameters, Field(alias="titleParameters")] diff --git a/streamdeck/models/events/touch_tap.py b/streamdeck/models/events/touch_tap.py index f02a5a7..f19cd3b 100644 --- a/streamdeck/models/events/touch_tap.py +++ b/streamdeck/models/events/touch_tap.py @@ -7,17 +7,16 @@ from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ( ContextualEventMixin, + CoordinatesPayloadMixin, DeviceSpecificEventMixin, PluginDefinedData, ) -class TouchTapPayload(ConfiguredBaseModel): +class TouchTapPayload(ConfiguredBaseModel, CoordinatesPayloadMixin): """Contextualized information for a TouchTap event.""" controller: Literal["Encoder"] """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" - coordinates: dict[Literal["column", "row"], int] - """Coordinates that identify the location of the action instance on the device.""" hold: bool """Determines whether the tap was considered 'held'.""" tap_position: Annotated[tuple[int, int], Field(alias="tapPos")] From edc307566159264162e17bce5c8402bf44fd6382 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Fri, 4 Apr 2025 15:04:51 -0600 Subject: [PATCH 12/15] Fix isInMultiAction alias in common.py --- streamdeck/models/events/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index d67e238..8b3b84d 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -74,7 +74,7 @@ class SingleActionPayload(ConfiguredBaseModel, CoordinatesPayloadMixin): 'Keypad' refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. 'Encoder' refers to a dial / touchscreen. """ - isInMultiAction: Literal[False] + is_in_multi_action: Annotated[Literal[False], Field(alias="isInMultiAction")] """Indicates that this event is not part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" @@ -92,7 +92,7 @@ class MultiActionPayload(ConfiguredBaseModel): Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. """ - isInMultiAction: Literal[True] + is_in_multi_action: Annotated[Literal[True], Field(alias="isInMultiAction")] """Indicates that this event is part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" From 1005474d2cb63389ddb5cba6861f67d5517c69cb Mon Sep 17 00:00:00 2001 From: strohganoff Date: Fri, 4 Apr 2025 15:24:41 -0600 Subject: [PATCH 13/15] Create Mixin classes for Single & Multi -Action payloads with the isInMultiAction literal boolean field. Create TypeAlias for discriminated Union field type for reuse --- streamdeck/models/events/common.py | 32 +++++++++++++++++++++----- streamdeck/models/events/keys.py | 16 ++++++------- streamdeck/models/events/settings.py | 3 ++- streamdeck/models/events/visibility.py | 5 ++-- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index 8b3b84d..e7b4a71 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -66,7 +66,31 @@ def coordinates(self) -> Coordinates: return Coordinates(**self.coordinates_obj) -class SingleActionPayload(ConfiguredBaseModel, CoordinatesPayloadMixin): +class SingleActionPayloadMixin: + """Mixin class for event models that have a single action payload.""" + + is_in_multi_action: Annotated[Literal[False], Field(alias="isInMultiAction")] + """Indicates that this event is not part of a multi-action.""" + + +class MultiActionPayloadMixin: + """Mixin class for event models that have a multi-action payload.""" + + is_in_multi_action: Annotated[Literal[True], Field(alias="isInMultiAction")] + """Indicates that this event is part of a multi-action.""" + + +CardinalityDiscriminated = Annotated[ + Union[ # noqa: UP007 + TypeVar("S", bound=SingleActionPayloadMixin), + TypeVar("M", bound=MultiActionPayloadMixin), + ], + Field(discriminator="is_in_multi_action"), +] +"""Generic type for a payload that either subclasses SingleActionPayloadMixin or MultiActionPayloadMixin—meaning it can be either a single action or a multi-action.""" + + +class SingleActionPayload(ConfiguredBaseModel, CoordinatesPayloadMixin, SingleActionPayloadMixin): """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are not part of a multi-action.""" controller: Literal["Encoder", "Keypad"] """Defines the controller type the action is applicable to. @@ -74,15 +98,13 @@ class SingleActionPayload(ConfiguredBaseModel, CoordinatesPayloadMixin): 'Keypad' refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. 'Encoder' refers to a dial / touchscreen. """ - is_in_multi_action: Annotated[Literal[False], Field(alias="isInMultiAction")] - """Indicates that this event is not part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" settings: PluginDefinedData """Settings associated with the action instance.""" -class MultiActionPayload(ConfiguredBaseModel): +class MultiActionPayload(ConfiguredBaseModel, MultiActionPayloadMixin): """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are part of a multi-action. NOTE: Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. @@ -92,8 +114,6 @@ class MultiActionPayload(ConfiguredBaseModel): Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. """ - is_in_multi_action: Annotated[Literal[True], Field(alias="isInMultiAction")] - """Indicates that this event is part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" settings: PluginDefinedData diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index d866d50..c3267b5 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -6,33 +6,31 @@ from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ( + CardinalityDiscriminated, ContextualEventMixin, CoordinatesPayloadMixin, DeviceSpecificEventMixin, - PluginDefinedData, + MultiActionPayloadMixin, + SingleActionPayloadMixin, ) ## Payload models used in the below KeyDown and KeyUp events -class SingleActionKeyGesturePayload(ConfiguredBaseModel, CoordinatesPayloadMixin): +class SingleActionKeyGesturePayload(ConfiguredBaseModel, SingleActionPayloadMixin, CoordinatesPayloadMixin): """Contextualized information for a KeyDown or KeyUp event that is not part of a multi-action.""" controller: Optional[Literal["Keypad"]] = None # noqa: UP007 """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" - is_in_multi_action: Annotated[Literal[False], Field(alias="isInMultiAction")] - """Indicates that this event is not part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" settings: PluginDefinedData """Settings associated with the action instance.""" -class MultiActionKeyGesturePayload(ConfiguredBaseModel): +class MultiActionKeyGesturePayload(ConfiguredBaseModel, MultiActionPayloadMixin): """Contextualized information for a KeyDown or KeyUp event that is part of a multi-action.""" controller: Optional[Literal["Keypad"]] = None # noqa: UP007 """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" - is_in_multi_action: Annotated[Literal[True], Field(alias="isInMultiAction")] - """Indicates that this event is part of a multi-action.""" state: Optional[int] = None # noqa: UP007 """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" user_desired_state: Annotated[int, Field(alias="userDesiredState")] @@ -48,12 +46,12 @@ class MultiActionKeyGesturePayload(ConfiguredBaseModel): class KeyDown(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user presses a action down.""" event: Literal["keyDown"] # type: ignore[override] - payload: Annotated[Union[SingleActionKeyGesturePayload, MultiActionKeyGesturePayload], Field(discriminator="isInMultiAction")] # noqa: UP007 + payload: CardinalityDiscriminated[SingleActionKeyGesturePayload, MultiActionKeyGesturePayload] """Contextualized information for this event.""" class KeyUp(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user releases a pressed action.""" event: Literal["keyUp"] # type: ignore[override] - payload: Annotated[Union[SingleActionKeyGesturePayload, MultiActionKeyGesturePayload], Field(discriminator="is_in_multi_action")] # noqa: UP007 + payload: CardinalityDiscriminated[SingleActionKeyGesturePayload, MultiActionKeyGesturePayload] """Contextualized information for this event.""" diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py index 9025a85..351e89d 100644 --- a/streamdeck/models/events/settings.py +++ b/streamdeck/models/events/settings.py @@ -6,6 +6,7 @@ from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ( + CardinalityDiscriminated, ContextualEventMixin, DeviceSpecificEventMixin, MultiActionPayload, @@ -17,7 +18,7 @@ class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the settings associated with an action instance are requested, or when the the settings were updated by the property inspector.""" event: Literal["didReceiveSettings"] # type: ignore[override] - payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="is_in_multi_action")] # noqa: UP007 + payload: CardinalityDiscriminated[SingleActionPayload, MultiActionPayload] """Contextualized information for this event.""" diff --git a/streamdeck/models/events/visibility.py b/streamdeck/models/events/visibility.py index 09bc396..55835ea 100644 --- a/streamdeck/models/events/visibility.py +++ b/streamdeck/models/events/visibility.py @@ -6,6 +6,7 @@ from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ( + CardinalityDiscriminated, ContextualEventMixin, DeviceSpecificEventMixin, MultiActionPayload, @@ -22,7 +23,7 @@ class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. """ event: Literal["willAppear"] # type: ignore[override] - payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="is_in_multi_action")] # noqa: UP007 + payload: CardinalityDiscriminated[SingleActionPayload, MultiActionPayload] class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): @@ -31,4 +32,4 @@ class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. """ event: Literal["willDisappear"] # type: ignore[override] - payload: Annotated[Union[SingleActionPayload, MultiActionPayload], Field(discriminator="is_in_multi_action")] # noqa: UP007 + payload: CardinalityDiscriminated[SingleActionPayload, MultiActionPayload] From 593bbca36c868db7c87ad41b153896b7fb395bf3 Mon Sep 17 00:00:00 2001 From: strohganoff Date: Fri, 4 Apr 2025 15:41:22 -0600 Subject: [PATCH 14/15] Create Controller type TypeAliases, and generic base classes to define which controller type is used by a payload model --- streamdeck/models/events/common.py | 72 +++++++++++--------- streamdeck/models/events/dials.py | 16 ++--- streamdeck/models/events/keys.py | 25 +++---- streamdeck/models/events/settings.py | 26 +++++-- streamdeck/models/events/title_parameters.py | 12 ++-- streamdeck/models/events/touch_tap.py | 9 ++- streamdeck/models/events/visibility.py | 23 +++++-- 7 files changed, 97 insertions(+), 86 deletions(-) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index e7b4a71..99235b5 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -1,6 +1,10 @@ from __future__ import annotations -from typing import Any, Literal, Optional +from abc import ABC +from typing import Annotated, Any, Generic, Literal, NamedTuple, Optional, Union + +from pydantic import Field +from typing_extensions import TypedDict, TypeVar from streamdeck.models.events.base import ConfiguredBaseModel @@ -23,6 +27,17 @@ class DeviceSpecificEventMixin: ## Payload models and metadata used by multiple event models. + +EncoderControllerType = Literal["Encoder"] +"""The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" +KeypadControllerType = Literal["Keypad"] +"""The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" +ControllerType = Literal[EncoderControllerType, KeypadControllerType] +"""Defines the controller type the action is applicable to.""" + +CT = TypeVar("CT", bound=ControllerType, default=ControllerType) + + PluginDefinedData = dict[str, Any] """Data of arbitrary structure that is defined in and relevant to the plugin.""" @@ -66,6 +81,28 @@ def coordinates(self) -> Coordinates: return Coordinates(**self.coordinates_obj) +class BasePayload(ConfiguredBaseModel, Generic[CT], ABC): + """Base class for all complex payload models.""" + controller: CT + """Defines the controller type the action is applicable to. + + 'Keypad' refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. + 'Encoder' refers to a dial / touchscreen on a 'Stream Deck +' device. + """ + settings: PluginDefinedData + """Settings associated with the action instance.""" + + +class BaseActionPayload(BasePayload[CT], ABC): + """Base class for payloads of action events.""" + + state: Optional[int] = None # noqa: UP007 + """Current state of the action. + + Only applicable to actions that have multiple states defined within the manifest.json file. + """ + + class SingleActionPayloadMixin: """Mixin class for event models that have a single action payload.""" @@ -88,36 +125,3 @@ class MultiActionPayloadMixin: Field(discriminator="is_in_multi_action"), ] """Generic type for a payload that either subclasses SingleActionPayloadMixin or MultiActionPayloadMixin—meaning it can be either a single action or a multi-action.""" - - -class SingleActionPayload(ConfiguredBaseModel, CoordinatesPayloadMixin, SingleActionPayloadMixin): - """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are not part of a multi-action.""" - controller: Literal["Encoder", "Keypad"] - """Defines the controller type the action is applicable to. - - 'Keypad' refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. - 'Encoder' refers to a dial / touchscreen. - """ - state: Optional[int] = None # noqa: UP007 - """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - settings: PluginDefinedData - """Settings associated with the action instance.""" - - -class MultiActionPayload(ConfiguredBaseModel, MultiActionPayloadMixin): - """Contextualized information for a willAppear, willDisappear, and didReceiveSettings events that are part of a multi-action. - - NOTE: Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. - """ - controller: Literal["Keypad"] - """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. - - Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. - """ - state: Optional[int] = None # noqa: UP007 - """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - settings: PluginDefinedData - """Settings associated with the action instance.""" - - - diff --git a/streamdeck/models/events/dials.py b/streamdeck/models/events/dials.py index f87f09c..ba63ada 100644 --- a/streamdeck/models/events/dials.py +++ b/streamdeck/models/events/dials.py @@ -2,34 +2,28 @@ from typing import Literal -from streamdeck.models.events.base import ConfiguredBaseModel, EventBase +from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ( + BasePayload, ContextualEventMixin, CoordinatesPayloadMixin, DeviceSpecificEventMixin, - PluginDefinedData, + EncoderControllerType, ) ## Payload models used in the below DialDown, DialRotate, and DialUp events -class EncoderPayload(ConfiguredBaseModel, CoordinatesPayloadMixin): +class EncoderPayload(BasePayload[EncoderControllerType], CoordinatesPayloadMixin): """Contextualized information for a DialDown or DialUp event.""" - controller: Literal["Encoder"] - """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" - settings: PluginDefinedData -class DialRotatePayload(ConfiguredBaseModel, CoordinatesPayloadMixin): +class DialRotatePayload(EncoderPayload): """Contextualized information for a DialRotate event.""" - controller: Literal["Encoder"] - """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" pressed: bool """Determines whether the dial was pressed whilst the rotation occurred.""" ticks: int """Number of ticks the dial was rotated; this can be a positive (clockwise) or negative (counter-clockwise) number.""" - settings: PluginDefinedData - """Settings associated with the action instance.""" ## Event models for DialDown, DialRotate, and DialUp events diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index c3267b5..ec31ff9 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -1,15 +1,17 @@ from __future__ import annotations -from typing import Annotated, Literal, Optional, Union +from typing import Annotated, Literal from pydantic import Field -from streamdeck.models.events.base import ConfiguredBaseModel, EventBase +from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ( + BaseActionPayload, CardinalityDiscriminated, ContextualEventMixin, CoordinatesPayloadMixin, DeviceSpecificEventMixin, + KeypadControllerType, MultiActionPayloadMixin, SingleActionPayloadMixin, ) @@ -17,28 +19,21 @@ ## Payload models used in the below KeyDown and KeyUp events -class SingleActionKeyGesturePayload(ConfiguredBaseModel, SingleActionPayloadMixin, CoordinatesPayloadMixin): +# It seems like for keyDown and keyUp events, the "controller" field is probably always missing, despite being defined in the official documentation. +OptionalKeyControllerTypeField = Annotated[KeypadControllerType, Field(default=None)] + + +class SingleActionKeyGesturePayload(BaseActionPayload[OptionalKeyControllerTypeField], SingleActionPayloadMixin, CoordinatesPayloadMixin): """Contextualized information for a KeyDown or KeyUp event that is not part of a multi-action.""" - controller: Optional[Literal["Keypad"]] = None # noqa: UP007 - """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" - state: Optional[int] = None # noqa: UP007 - """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - settings: PluginDefinedData - """Settings associated with the action instance.""" -class MultiActionKeyGesturePayload(ConfiguredBaseModel, MultiActionPayloadMixin): +class MultiActionKeyGesturePayload(BaseActionPayload[OptionalKeyControllerTypeField], MultiActionPayloadMixin): """Contextualized information for a KeyDown or KeyUp event that is part of a multi-action.""" - controller: Optional[Literal["Keypad"]] = None # noqa: UP007 - """The 'Keypad' controller type refers to a standard action on a Stream Deck device, e.g. buttons or a pedal.""" - state: Optional[int] = None # noqa: UP007 - """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" user_desired_state: Annotated[int, Field(alias="userDesiredState")] """Desired state as specified by the user. Only applicable to actions that have multiple states defined within the manifest.json file, and when this action instance is part of a multi-action. """ - settings: PluginDefinedData ## Event models for KeyDown and KeyUp events diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py index 351e89d..8839f9a 100644 --- a/streamdeck/models/events/settings.py +++ b/streamdeck/models/events/settings.py @@ -1,24 +1,38 @@ from __future__ import annotations -from typing import Annotated, Literal, Union - -from pydantic import Field +from typing import Literal from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ( + BaseActionPayload, CardinalityDiscriminated, ContextualEventMixin, + CoordinatesPayloadMixin, DeviceSpecificEventMixin, - MultiActionPayload, + KeypadControllerType, + MultiActionPayloadMixin, PluginDefinedData, - SingleActionPayload, + SingleActionPayloadMixin, ) +## Models for didReceiveSettings event and its specific payloads. + +class SingleActionSettingsPayload(BaseActionPayload, SingleActionPayloadMixin, CoordinatesPayloadMixin): + """Contextualized information for a didReceiveSettings events that are not part of a multi-action.""" + + +class MultiActionSettingsPayload(BaseActionPayload[KeypadControllerType], MultiActionPayloadMixin): + """Contextualized information for a didReceiveSettings events that are part of a multi-action. + + NOTE: Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. + """ + + class DidReceiveSettings(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the settings associated with an action instance are requested, or when the the settings were updated by the property inspector.""" event: Literal["didReceiveSettings"] # type: ignore[override] - payload: CardinalityDiscriminated[SingleActionPayload, MultiActionPayload] + payload: CardinalityDiscriminated[SingleActionSettingsPayload, MultiActionSettingsPayload] """Contextualized information for this event.""" diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index 62b207c..02fec5e 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -1,14 +1,14 @@ from __future__ import annotations -from typing import Annotated, Literal, Optional +from typing import Annotated, Literal from pydantic import Field from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ( + BaseActionPayload, CoordinatesPayloadMixin, DeviceSpecificEventMixin, - PluginDefinedData, ) @@ -34,17 +34,13 @@ class TitleParameters(ConfiguredBaseModel): """Color of the title, represented as a hexadecimal value.""" -class TitleParametersDidChangePayload(ConfiguredBaseModel, CoordinatesPayloadMixin): +class TitleParametersDidChangePayload(BaseActionPayload, CoordinatesPayloadMixin): """Contextualized information for this event.""" - controller: Literal["Keypad", "Encoder"] - """Defines the controller type the action is applicable to.""" title: str """Title of the action, as specified by the user or dynamically by the plugin.""" title_parameters: Annotated[TitleParameters, Field(alias="titleParameters")] """Defines aesthetic properties that determine how the title should be rendered.""" - state: Optional[int] # noqa: UP007 - """Current state of the action; only applicable to actions that have multiple states defined within the manifest.json file.""" - settings: PluginDefinedData + class TitleParametersDidChange(EventBase, DeviceSpecificEventMixin): """Occurs when the user updates an action's title settings in the Stream Deck application.""" diff --git a/streamdeck/models/events/touch_tap.py b/streamdeck/models/events/touch_tap.py index f19cd3b..4226a1c 100644 --- a/streamdeck/models/events/touch_tap.py +++ b/streamdeck/models/events/touch_tap.py @@ -6,22 +6,21 @@ from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ( + BasePayload, ContextualEventMixin, CoordinatesPayloadMixin, DeviceSpecificEventMixin, - PluginDefinedData, + EncoderControllerType, ) -class TouchTapPayload(ConfiguredBaseModel, CoordinatesPayloadMixin): +class TouchTapPayload(BasePayload[EncoderControllerType], CoordinatesPayloadMixin): """Contextualized information for a TouchTap event.""" - controller: Literal["Encoder"] - """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" hold: bool """Determines whether the tap was considered 'held'.""" tap_position: Annotated[tuple[int, int], Field(alias="tapPos")] """Coordinates of where the touchscreen tap occurred, relative to the canvas of the action.""" - settings: PluginDefinedData + class TouchTap(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): """Occurs when the user taps the touchscreen (Stream Deck +).""" diff --git a/streamdeck/models/events/visibility.py b/streamdeck/models/events/visibility.py index 55835ea..7ab38f1 100644 --- a/streamdeck/models/events/visibility.py +++ b/streamdeck/models/events/visibility.py @@ -1,19 +1,28 @@ from __future__ import annotations -from typing import Annotated, Literal, Union - -from pydantic import Field +from typing import Literal from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ( + BaseActionPayload, CardinalityDiscriminated, ContextualEventMixin, + CoordinatesPayloadMixin, DeviceSpecificEventMixin, - MultiActionPayload, - SingleActionPayload, + KeypadControllerType, + MultiActionPayloadMixin, + SingleActionPayloadMixin, ) +class SingleActionVisibilityPayload(BaseActionPayload, SingleActionPayloadMixin, CoordinatesPayloadMixin): + """Contextualized information for willAppear and willDisappear events that is not part of a multi-action.""" + + +class MultiActionVisibilityPayload(BaseActionPayload[KeypadControllerType], MultiActionPayloadMixin): + """Contextualized information for willAppear and willDisappear events that is part of a multi-action.""" + + ## Event models for WillAppear and WillDisappear events class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): @@ -23,7 +32,7 @@ class WillAppear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. """ event: Literal["willAppear"] # type: ignore[override] - payload: CardinalityDiscriminated[SingleActionPayload, MultiActionPayload] + payload: CardinalityDiscriminated[SingleActionVisibilityPayload, MultiActionVisibilityPayload] class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): @@ -32,4 +41,4 @@ class WillDisappear(EventBase, ContextualEventMixin, DeviceSpecificEventMixin): An action refers to all types of actions, e.g. keys, dials, touchscreens, pedals, etc. """ event: Literal["willDisappear"] # type: ignore[override] - payload: CardinalityDiscriminated[SingleActionPayload, MultiActionPayload] + payload: CardinalityDiscriminated[SingleActionVisibilityPayload, MultiActionVisibilityPayload] From 024bfbf8be02d79b3eb65b5e891e5e2a748cff3e Mon Sep 17 00:00:00 2001 From: strohganoff Date: Mon, 7 Apr 2025 14:25:14 -0600 Subject: [PATCH 15/15] Finish design of BasePayload model and mixin classes, including new mixin that provides the attribute --- streamdeck/models/events/common.py | 33 ++++++++++---------- streamdeck/models/events/keys.py | 16 ++++++++-- streamdeck/models/events/settings.py | 17 ++++++++-- streamdeck/models/events/title_parameters.py | 9 ++++-- streamdeck/models/events/visibility.py | 16 ++++++++-- 5 files changed, 63 insertions(+), 28 deletions(-) diff --git a/streamdeck/models/events/common.py b/streamdeck/models/events/common.py index 99235b5..8ecc281 100644 --- a/streamdeck/models/events/common.py +++ b/streamdeck/models/events/common.py @@ -28,6 +28,10 @@ class DeviceSpecificEventMixin: ## Payload models and metadata used by multiple event models. +PluginDefinedData = dict[str, Any] +"""Data of arbitrary structure that is defined in and relevant to the plugin.""" + + EncoderControllerType = Literal["Encoder"] """The 'Encoder' controller type refers to a dial or touchscreen on a 'Stream Deck +' device.""" KeypadControllerType = Literal["Keypad"] @@ -38,8 +42,16 @@ class DeviceSpecificEventMixin: CT = TypeVar("CT", bound=ControllerType, default=ControllerType) -PluginDefinedData = dict[str, Any] -"""Data of arbitrary structure that is defined in and relevant to the plugin.""" +class BasePayload(ConfiguredBaseModel, Generic[CT], ABC): + """Base class for all complex payload models.""" + controller: CT + """Defines the controller type the action is applicable to. + + 'Keypad' refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. + 'Encoder' refers to a dial / touchscreen on a 'Stream Deck +' device. + """ + settings: PluginDefinedData + """Settings associated with the action instance.""" class CoordinatesDict(TypedDict): @@ -81,21 +93,8 @@ def coordinates(self) -> Coordinates: return Coordinates(**self.coordinates_obj) -class BasePayload(ConfiguredBaseModel, Generic[CT], ABC): - """Base class for all complex payload models.""" - controller: CT - """Defines the controller type the action is applicable to. - - 'Keypad' refers to a standard action on a Stream Deck device, e.g. buttons or a pedal. - 'Encoder' refers to a dial / touchscreen on a 'Stream Deck +' device. - """ - settings: PluginDefinedData - """Settings associated with the action instance.""" - - -class BaseActionPayload(BasePayload[CT], ABC): - """Base class for payloads of action events.""" - +class StatefulActionPayloadMixin: + """Mixin class for payload models that have an optional state field.""" state: Optional[int] = None # noqa: UP007 """Current state of the action. diff --git a/streamdeck/models/events/keys.py b/streamdeck/models/events/keys.py index ec31ff9..6b74921 100644 --- a/streamdeck/models/events/keys.py +++ b/streamdeck/models/events/keys.py @@ -6,7 +6,7 @@ from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ( - BaseActionPayload, + BasePayload, CardinalityDiscriminated, ContextualEventMixin, CoordinatesPayloadMixin, @@ -14,6 +14,7 @@ KeypadControllerType, MultiActionPayloadMixin, SingleActionPayloadMixin, + StatefulActionPayloadMixin, ) @@ -23,11 +24,20 @@ OptionalKeyControllerTypeField = Annotated[KeypadControllerType, Field(default=None)] -class SingleActionKeyGesturePayload(BaseActionPayload[OptionalKeyControllerTypeField], SingleActionPayloadMixin, CoordinatesPayloadMixin): +class SingleActionKeyGesturePayload( + BasePayload[OptionalKeyControllerTypeField], + SingleActionPayloadMixin, + StatefulActionPayloadMixin, + CoordinatesPayloadMixin, +): """Contextualized information for a KeyDown or KeyUp event that is not part of a multi-action.""" -class MultiActionKeyGesturePayload(BaseActionPayload[OptionalKeyControllerTypeField], MultiActionPayloadMixin): +class MultiActionKeyGesturePayload( + BasePayload[OptionalKeyControllerTypeField], + MultiActionPayloadMixin, + StatefulActionPayloadMixin, +): """Contextualized information for a KeyDown or KeyUp event that is part of a multi-action.""" user_desired_state: Annotated[int, Field(alias="userDesiredState")] """Desired state as specified by the user. diff --git a/streamdeck/models/events/settings.py b/streamdeck/models/events/settings.py index 8839f9a..c9dab53 100644 --- a/streamdeck/models/events/settings.py +++ b/streamdeck/models/events/settings.py @@ -4,7 +4,7 @@ from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ( - BaseActionPayload, + BasePayload, CardinalityDiscriminated, ContextualEventMixin, CoordinatesPayloadMixin, @@ -13,16 +13,27 @@ MultiActionPayloadMixin, PluginDefinedData, SingleActionPayloadMixin, + StatefulActionPayloadMixin, ) ## Models for didReceiveSettings event and its specific payloads. -class SingleActionSettingsPayload(BaseActionPayload, SingleActionPayloadMixin, CoordinatesPayloadMixin): + +class SingleActionSettingsPayload( + BasePayload, + SingleActionPayloadMixin, + StatefulActionPayloadMixin, + CoordinatesPayloadMixin, +): """Contextualized information for a didReceiveSettings events that are not part of a multi-action.""" -class MultiActionSettingsPayload(BaseActionPayload[KeypadControllerType], MultiActionPayloadMixin): +class MultiActionSettingsPayload( + BasePayload[KeypadControllerType], + MultiActionPayloadMixin, + StatefulActionPayloadMixin, +): """Contextualized information for a didReceiveSettings events that are part of a multi-action. NOTE: Action instances that are part of a multi-action are only applicable to the 'Keypad' controller type. diff --git a/streamdeck/models/events/title_parameters.py b/streamdeck/models/events/title_parameters.py index 02fec5e..386d089 100644 --- a/streamdeck/models/events/title_parameters.py +++ b/streamdeck/models/events/title_parameters.py @@ -6,9 +6,10 @@ from streamdeck.models.events.base import ConfiguredBaseModel, EventBase from streamdeck.models.events.common import ( - BaseActionPayload, + BasePayload, CoordinatesPayloadMixin, DeviceSpecificEventMixin, + StatefulActionPayloadMixin, ) @@ -34,7 +35,11 @@ class TitleParameters(ConfiguredBaseModel): """Color of the title, represented as a hexadecimal value.""" -class TitleParametersDidChangePayload(BaseActionPayload, CoordinatesPayloadMixin): +class TitleParametersDidChangePayload( + BasePayload, + CoordinatesPayloadMixin, + StatefulActionPayloadMixin, +): """Contextualized information for this event.""" title: str """Title of the action, as specified by the user or dynamically by the plugin.""" diff --git a/streamdeck/models/events/visibility.py b/streamdeck/models/events/visibility.py index 7ab38f1..cf35c34 100644 --- a/streamdeck/models/events/visibility.py +++ b/streamdeck/models/events/visibility.py @@ -4,7 +4,7 @@ from streamdeck.models.events.base import EventBase from streamdeck.models.events.common import ( - BaseActionPayload, + BasePayload, CardinalityDiscriminated, ContextualEventMixin, CoordinatesPayloadMixin, @@ -12,14 +12,24 @@ KeypadControllerType, MultiActionPayloadMixin, SingleActionPayloadMixin, + StatefulActionPayloadMixin, ) -class SingleActionVisibilityPayload(BaseActionPayload, SingleActionPayloadMixin, CoordinatesPayloadMixin): +class SingleActionVisibilityPayload( + BasePayload, + SingleActionPayloadMixin, + StatefulActionPayloadMixin, + CoordinatesPayloadMixin, +): """Contextualized information for willAppear and willDisappear events that is not part of a multi-action.""" -class MultiActionVisibilityPayload(BaseActionPayload[KeypadControllerType], MultiActionPayloadMixin): +class MultiActionVisibilityPayload( + BasePayload[KeypadControllerType], + MultiActionPayloadMixin, + StatefulActionPayloadMixin, +): """Contextualized information for willAppear and willDisappear events that is part of a multi-action."""