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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,24 @@ def resolve_api_client(event_data):
return api_client
```

The resolver can return a module-level instance or compute an event-specific value. `scope` defaults to `Plugin` when omitted.
The resolver can return a module-level instance or compute an event-specific value. Its `event_data` argument is the event being handled. `scope` declares what identity the event must provide for the parameter to be injected; it defaults to `Plugin` when omitted:

- `Plugin`: no device or action-instance identity is needed, so the parameter is available for every event.
- `Device`: the event must identify a device. Device events and action-instance events that include a device qualify.
- `ActionInstance`: the event must identify an action instance; a device is optional.
- `ActionInstanceWithDevice`: the event must identify both an action instance and a device.

`Device` and `ActionInstance` are separate identity capabilities. `ActionInstanceWithDevice` provides both, so it can satisfy either requirement. When handlers are registered, the SDK checks their event types against the requested scope and raises `TypeError` if an event cannot supply the required identity.

The SDK also provides built-in context parameters, which do not need a resolver entry in `pyproject.toml`. Request one by adding its name to an action handler's signature:

```python
def handle_will_appear(event_data, action_instance_context):
settings = action_instance_context.settings
current_state = action_instance_context.state
```

The SDK seeds `plugin_context` with launch information and updates the contexts from relevant Stream Deck events before user handlers run. `plugin_context` includes the plugin UUID, registration UUID, launch info, and global settings. `device_context` includes a device ID, connection status, and the latest device information. `action_context` identifies an action by UUID. `action_instance_context` identifies one action instance and holds its latest known settings, state, controller, coordinates, and associated action and device. Until an event supplies a value, context fields retain their empty or `None` defaults. The SDK rejects handlers that request a context unavailable for one of their event types.



Expand Down
2 changes: 2 additions & 0 deletions streamdeck/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import (
command_sender,
contexts,
manager,
models,
utils,
Expand All @@ -11,6 +12,7 @@
__all__ = [
"actions",
"command_sender",
"contexts",
"manager",
"models",
"utils",
Expand Down
31 changes: 16 additions & 15 deletions streamdeck/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,29 @@

import logging
import sys
from typing import TYPE_CHECKING, TypeVar

from pydantic import ValidationError
from pydantic_settings import SettingsError
from pydantic_settings import CliApp, SettingsError

from streamdeck.contexts import (
ContextStore,
register_context_handlers,
register_context_injectables,
)
from streamdeck.manager import PluginManager
from streamdeck.models.configs import StreamDeckToolConfig
from streamdeck.models.launch import LaunchArguments
from streamdeck.utils.logging import configure_streamdeck_logger


logger = logging.getLogger("streamdeck")
SettingsT = TypeVar("SettingsT")

if TYPE_CHECKING:
from collections.abc import Callable


def _parse_settings_from_cli(
settings_cls: Callable[..., SettingsT], args: list[str]
) -> SettingsT:
"""Instantiate Pydantic Settings with its private CLI argument hook."""
return settings_cls(_cli_parse_args=args)


def main(argv: list[str] | None = None) -> None:
"""Console entry point for the Pydantic Settings CLI."""
args = sys.argv[1:] if argv is None else list(argv)
try:
launch = _parse_settings_from_cli(LaunchArguments, args)
launch = CliApp.run(LaunchArguments, cli_args=args)
except (ValidationError, SettingsError) as exc:
logger.error("Invalid launch arguments: %s", exc)
raise SystemExit(2) from exc
Expand All @@ -50,7 +43,7 @@ def main(argv: list[str] | None = None) -> None:
if launch.debug_port:
setup_debug_mode(launch.debug_port)

config = _parse_settings_from_cli(StreamDeckToolConfig, args)
config = CliApp.run(StreamDeckToolConfig, cli_args=args)
run_plugin(launch, config)
except Exception:
logger.exception("Plugin startup or execution failed")
Expand All @@ -76,13 +69,21 @@ def run_plugin(launch: LaunchArguments, config: StreamDeckToolConfig) -> None:
register_event=launch.register_event,
)

context_store = ContextStore(
plugin_registration_uuid=launch.plugin_registration_uuid,
info=launch.info,
)
manager.register_action(register_context_handlers(context_store))

for name, param_config in config.injectable_handler_params.items():
manager.register_injectable_handler_param(
name,
param_config.resolver,
scope=param_config.scope,
)

register_context_injectables(context_store, manager.register_injectable_handler_param)

# Register custom event types before validating action handlers.
for listener_type in config.event_listeners:
manager.register_event_listener(listener_type())
Expand Down
Loading
Loading