Skip to content

Commit a2ed941

Browse files
authored
Merge pull request #16 from strohganoff/refactor/eventbase-subscripting
Refactor/eventbase subscripting
2 parents 8405877 + 1d98b3b commit a2ed941

33 files changed

+562
-315
lines changed

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ The SDK allows you to create custom event listeners and events by extending the
292292
293293
To create a custom event listener:
294294
295-
1. Create new event model that inherits from `EventBase`.
295+
1. Create new event model that inherits from `EventBase["eventName"]`.
296296
2. Create a new class that inherits from `EventListener`.
297297
a. Implement the required `listen` and `stop` methods. The `listen` method should yield results as a json string that matches the new event model.
298298
b. List the new event classes in the `event_models` class variable of the new `EventListener` class.
@@ -307,11 +307,15 @@ from streamdeck.event_listener import EventListener
307307
from streamdeck.models.events import EventBase
308308
309309
310-
class MyCustomEvent(EventBase):
311-
event: Literal["somethingHappened"]
312-
... # Define additional data attributes here
310+
class MyCustomEvent(EventBase["somethingHappened"]):
311+
# The 'event' field's type annotation is internally set as Literal["somethingHappened"]
312+
# Define additional data attributes here
313+
result: str
314+
313315

314316
class MyCustomEventListener(EventListener):
317+
event_models = [MyCustomEvent]
318+
315319
def listen(self) -> Generator[str | bytes, None, None]:
316320
...
317321
# Listen/poll for something here in a loop, and yield the result.
@@ -320,7 +324,7 @@ class MyCustomEventListener(EventListener):
320324
# while self._running is True:
321325
# result = module.check_status()
322326
# if result is not None:
323-
# yield json.dumps({"event": "somethingHappend", "result": result})
327+
# yield json.dumps({"event": "somethingHappened", "result": result})
324328
# time.sleep(1)
325329

326330
def stop(self) -> None:
@@ -344,7 +348,7 @@ To use your custom event listener, add it to your `pyproject.toml` file:
344348
]
345349
```
346350
347-
The `event_listeners` list should contain strings in module format for each module you want to use.
351+
The `event_listener_modules` list should contain strings in module format for each module you want to use.
348352
349353
350354
## Creating and Packaging Plugins

streamdeck/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
command_sender,
44
manager,
55
models,
6-
types,
76
utils,
87
websocket,
98
)
@@ -14,7 +13,6 @@
1413
"command_sender",
1514
"manager",
1615
"models",
17-
"types",
1816
"utils",
1917
"websocket",
2018
]

streamdeck/actions.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,22 @@
99

1010
if TYPE_CHECKING:
1111
from collections.abc import Callable, Generator
12+
from typing import Protocol
13+
14+
from typing_extensions import ParamSpec, TypeVar
1215

1316
from streamdeck.models.events import EventBase
14-
from streamdeck.models.events.base import LiteralStrGenericAlias
15-
from streamdeck.types import BaseEventHandlerFunc, EventHandlerFunc, EventNameStr, TEvent_contra
17+
from streamdeck.types import ActionUUIDStr, EventNameStr
18+
19+
20+
21+
EventModel_contra = TypeVar("EventModel_contra", bound=EventBase, default=EventBase, contravariant=True)
22+
InjectableParams = ParamSpec("InjectableParams", default=...)
23+
24+
class EventHandlerFunc(Protocol[EventModel_contra, InjectableParams]):
25+
"""Protocol for an event handler function that takes an event (of subtype of EventBase) and other parameters that are injectable."""
26+
def __call__(self, event_data: EventModel_contra, *args: InjectableParams.args, **kwargs: InjectableParams.kwargs) -> None: ...
27+
1628

1729

1830
logger = getLogger("streamdeck.actions")
@@ -22,14 +34,10 @@ class ActionBase(ABC):
2234
"""Base class for all actions."""
2335

2436
def __init__(self) -> None:
25-
"""Initialize an Action instance.
37+
"""Initialize an Action instance."""
38+
self._events: dict[EventNameStr, set[EventHandlerFunc]] = defaultdict(set)
2639

27-
Args:
28-
uuid (str): The unique identifier for the action.
29-
"""
30-
self._events: dict[EventNameStr, set[BaseEventHandlerFunc]] = defaultdict(set)
31-
32-
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[TEvent_contra] | BaseEventHandlerFunc], EventHandlerFunc[TEvent_contra] | BaseEventHandlerFunc]:
40+
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[EventModel_contra, InjectableParams]], EventHandlerFunc[EventModel_contra, InjectableParams]]:
3341
"""Register an event handler for a specific event.
3442
3543
Args:
@@ -41,15 +49,14 @@ def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc[TEvent_c
4149
Raises:
4250
KeyError: If the provided event name is not available.
4351
"""
44-
def _wrapper(func: EventHandlerFunc[TEvent_contra]) -> EventHandlerFunc[TEvent_contra]:
45-
# Cast to BaseEventHandlerFunc so that the storage type is consistent.
46-
self._events[event_name].add(cast("BaseEventHandlerFunc", func))
47-
52+
def _wrapper(func: EventHandlerFunc[EventModel_contra, InjectableParams]) -> EventHandlerFunc[EventModel_contra, InjectableParams]:
53+
# Cast to EventHandlerFunc with default type arguments so that the storage type is consistent.
54+
self._events[event_name].add(cast("EventHandlerFunc", func))
4855
return func
4956

5057
return _wrapper
5158

52-
def get_event_handlers(self, event_name: EventNameStr, /) -> Generator[EventHandlerFunc[EventBase[LiteralStrGenericAlias]], None, None]:
59+
def get_event_handlers(self, event_name: EventNameStr, /) -> Generator[EventHandlerFunc, None, None]:
5360
"""Get all event handlers for a specific event.
5461
5562
Args:
@@ -66,22 +73,23 @@ def get_event_handlers(self, event_name: EventNameStr, /) -> Generator[EventHand
6673

6774
yield from self._events[event_name]
6875

69-
def get_registered_event_names(self) -> list[str]:
76+
def get_registered_event_names(self) -> list[EventNameStr]:
7077
"""Get all event names for which event handlers are registered.
7178
7279
Returns:
7380
list[str]: The list of event names for which event handlers are registered.
7481
"""
7582
return list(self._events.keys())
7683

84+
7785
class GlobalAction(ActionBase):
7886
"""Represents an action that is performed at the plugin level, meaning it isn't associated with a specific device or action."""
7987

8088

8189
class Action(ActionBase):
8290
"""Represents an action that can be performed for a specific action, with event handlers for specific event types."""
8391

84-
def __init__(self, uuid: str) -> None:
92+
def __init__(self, uuid: ActionUUIDStr) -> None:
8593
"""Initialize an Action instance.
8694
8795
Args:
@@ -111,7 +119,7 @@ def register(self, action: ActionBase) -> None:
111119
"""
112120
self._plugin_actions.append(action)
113121

114-
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: str | None = None) -> Generator[EventHandlerFunc[EventBase[LiteralStrGenericAlias]], None, None]:
122+
def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: ActionUUIDStr | None = None) -> Generator[EventHandlerFunc, None, None]:
115123
"""Get all event handlers for a specific event from all registered actions.
116124
117125
Args:

streamdeck/command_sender.py

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
if TYPE_CHECKING:
88
from typing import Any, Literal
99

10+
from streamdeck.types import (
11+
ActionInstanceUUIDStr,
12+
ActionUUIDStr,
13+
DeviceUUIDStr,
14+
EventNameStr,
15+
PluginDefinedData,
16+
)
1017
from streamdeck.websocket import WebSocketClient
1118

1219

@@ -16,30 +23,31 @@
1623

1724
class StreamDeckCommandSender:
1825
"""Class for sending command event messages to the Stream Deck software through a WebSocket client."""
26+
1927
def __init__(self, client: WebSocketClient, plugin_registration_uuid: str):
2028
self._client = client
2129
self._plugin_registration_uuid = plugin_registration_uuid
2230

23-
def _send_event(self, event: str, **kwargs: Any) -> None:
31+
def _send_event(self, event: EventNameStr, **kwargs: Any) -> None:
2432
self._client.send_event({
2533
"event": event,
2634
**kwargs,
2735
})
2836

29-
def set_settings(self, context: str, payload: dict[str, Any]) -> None:
37+
def set_settings(self, context: ActionInstanceUUIDStr, payload: PluginDefinedData) -> None:
3038
self._send_event(
3139
event="setSettings",
3240
context=context,
3341
payload=payload,
3442
)
3543

36-
def get_settings(self, context: str) -> None:
44+
def get_settings(self, context: ActionInstanceUUIDStr) -> None:
3745
self._send_event(
3846
event="getSettings",
3947
context=context,
4048
)
4149

42-
def set_global_settings(self, payload: dict[str, Any]) -> None:
50+
def set_global_settings(self, payload: PluginDefinedData) -> None:
4351
self._send_event(
4452
event="setGlobalSettings",
4553
context=self._plugin_registration_uuid,
@@ -52,14 +60,14 @@ def get_global_settings(self) -> None:
5260
context=self._plugin_registration_uuid,
5361
)
5462

55-
def open_url(self, context: str, url: str) -> None:
63+
def open_url(self, context: ActionInstanceUUIDStr, url: str) -> None:
5664
self._send_event(
5765
event="openUrl",
5866
context=context,
5967
payload={"url": url},
6068
)
6169

62-
def log_message(self, context: str, message: str) -> None:
70+
def log_message(self, context: ActionInstanceUUIDStr, message: str) -> None:
6371
self._send_event(
6472
event="logMessage",
6573
context=context,
@@ -68,10 +76,10 @@ def log_message(self, context: str, message: str) -> None:
6876

6977
def set_title(
7078
self,
71-
context: str,
79+
context: ActionInstanceUUIDStr,
7280
state: int | None = None,
73-
target: str | None = None,
74-
title: str | None = None
81+
target: Literal["hardware", "software", "both"] | None = None,
82+
title: str | None = None,
7583
) -> None:
7684
payload = {}
7785

@@ -90,10 +98,10 @@ def set_title(
9098

9199
def set_image(
92100
self,
93-
context: str,
94-
image: str, # base64 encoded image,
95-
target: Literal["hardware", "software", "both"], # software, hardware, or both,
96-
state: int, # 0-based integer
101+
context: ActionInstanceUUIDStr,
102+
image: str, # base64 encoded image,
103+
target: Literal["hardware", "software", "both"],
104+
state: int,
97105
) -> None:
98106
"""...
99107
@@ -117,14 +125,26 @@ def set_image(
117125
},
118126
)
119127

120-
def set_feedback(self, context: str, payload: dict[str, Any]) -> None:
128+
def set_feedback(self, context: ActionInstanceUUIDStr, payload: PluginDefinedData) -> None:
129+
"""Set's the feedback of an existing layout associated with an action instance.
130+
131+
Args:
132+
context (str): Defines the context of the command, e.g. which action instance the command is intended for.
133+
payload (PluginDefinedData): Additional information supplied as part of the command.
134+
"""
121135
self._send_event(
122136
event="setFeedback",
123137
context=context,
124138
payload=payload,
125139
)
126140

127-
def set_feedback_layout(self, context: str, layout: str) -> None:
141+
def set_feedback_layout(self, context: ActionInstanceUUIDStr, layout: str) -> None:
142+
"""Sets the layout associated with an action instance.
143+
144+
Args:
145+
context (str): Defines the context of the command, e.g. which action instance the command is intended for.
146+
layout (str): Name of a pre-defined layout, or relative path to a custom one.
147+
"""
128148
self._send_event(
129149
event="setFeedbackLayout",
130150
context=context,
@@ -133,7 +153,7 @@ def set_feedback_layout(self, context: str, layout: str) -> None:
133153

134154
def set_trigger_description(
135155
self,
136-
context: str,
156+
context: ActionInstanceUUIDStr,
137157
rotate: str | None = None,
138158
push: str | None = None,
139159
touch: str | None = None,
@@ -170,21 +190,21 @@ def set_trigger_description(
170190
},
171191
)
172192

173-
def show_alert(self, context: str) -> None:
193+
def show_alert(self, context: ActionInstanceUUIDStr) -> None:
174194
"""Temporarily show an alert icon on the image displayed by an instance of an action."""
175195
self._send_event(
176196
event="showAlert",
177197
context=context,
178198
)
179199

180-
def show_ok(self, context: str) -> None:
200+
def show_ok(self, context: ActionInstanceUUIDStr) -> None:
181201
"""Temporarily show an OK checkmark icon on the image displayed by an instance of an action."""
182202
self._send_event(
183203
event="showOk",
184204
context=context,
185205
)
186206

187-
def set_state(self, context: str, state: int) -> None:
207+
def set_state(self, context: ActionInstanceUUIDStr, state: int) -> None:
188208
self._send_event(
189209
event="setState",
190210
context=context,
@@ -193,8 +213,8 @@ def set_state(self, context: str, state: int) -> None:
193213

194214
def switch_to_profile(
195215
self,
196-
context: str,
197-
device: str,
216+
context: ActionInstanceUUIDStr,
217+
device: DeviceUUIDStr,
198218
profile: str | None = None,
199219
page: int = 0,
200220
) -> None:
@@ -211,7 +231,7 @@ def switch_to_profile(
211231
page (int): Page to show when switching to the profile; indexed from 0.
212232
"""
213233
# TODO: Should validation happen that ensures the specified profile is declared in manifest.yaml?
214-
payload = {}
234+
payload: dict[str, str | int | None] = {}
215235

216236
if profile is not None:
217237
payload = {
@@ -226,18 +246,17 @@ def switch_to_profile(
226246
payload=payload,
227247
)
228248

229-
def send_to_property_inspector(self, context: str, payload: dict[str, Any]) -> None:
249+
def send_to_property_inspector(
250+
self, context: ActionInstanceUUIDStr, payload: PluginDefinedData
251+
) -> None:
230252
self._send_event(
231253
event="sendToPropertyInspector",
232254
context=context,
233255
payload=payload,
234256
)
235257

236258
def send_to_plugin(
237-
self,
238-
context: str,
239-
action: str,
240-
payload: dict[str, Any]
259+
self, context: ActionInstanceUUIDStr, action: ActionUUIDStr, payload: PluginDefinedData
241260
) -> None:
242261
"""Send a payload to another plugin.
243262

streamdeck/event_listener.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from typing_extensions import TypeIs
1515

1616
from streamdeck.models.events import EventBase
17-
from streamdeck.models.events.base import LiteralStrGenericAlias
1817

1918

2019

@@ -120,7 +119,7 @@ class EventListener(ABC):
120119
Event listeners are classes that listen for events and simply yield them as they come.
121120
The EventListenerManager will handle the threading and pushing the events yielded into a shared queue.
122121
"""
123-
event_models: ClassVar[list[type[EventBase[LiteralStrGenericAlias]]]]
122+
event_models: ClassVar[list[type[EventBase]]]
124123
"""A list of event models that the listener can yield. Read in by the PluginManager to model the incoming event data off of.
125124
126125
The plugin-developer must define this list in their subclass.

0 commit comments

Comments
 (0)