English · Русский
A typed Python SDK for building MiraBox Stream Dock plugins
Build reusable actions for keys, touch panels, and dials
without hand-writing the Stream Dock WebSocket protocol.
Quick start · PyPI · Example plugin · Protocol · API · Development
mirabox-stream-dock-sdk provides the protocol, runtime, and browser-side tools
needed to build Python plugins for MiraBox Stream Dock. It validates launch
arguments and incoming messages, creates one typed action instance per visible
control, dispatches lifecycle events, and serializes commands back to the
Stream Dock application.
The SDK was originally developed as part of a Stream Dock plugin. It was later extracted into a standalone project so the protocol and runtime could be reused across plugins, tested independently, and evolved as a public package. The SDK will continue to be improved as it is used in real plugins and more Stream Dock behavior is verified.
Important
The project is currently in the 0.x series. It is ready for experimentation
and real plugin development, but public APIs may evolve between minor releases
before 1.0.
Note
This is an unofficial community project and is not affiliated with or endorsed
by MiraBox, HotSpot, or Elgato. The callback name
connectElgatoStreamDeckSocket is retained because Stream Dock uses it for
Property Inspector compatibility.
- Features
- How it works
- Requirements
- Installation
- Quick start
- Property Inspector client
- Counter example plugin
- Protocol basis
- API overview
- Errors and unknown events
- Inbound event queue
- Outbound command bus
- Concurrency contract
- Logging
- Project structure
- Development
- Releasing
| Feature | What it provides | |
|---|---|---|
| 🧩 | Typed protocol | Dataclass models for registration, commands, and key, touch, dial, device, application, and settings events. |
| 🧭 | Precise validation | Malformed payloads report the event name and exact JSON field path that failed validation. |
| 🎛️ | Action runtime | One action instance per Stream Dock context, declarative UUID registration, and automatic lifecycle dispatch. |
| 🔌 | WebSocket transport | Registration, message parsing, command serialization, logging, and graceful shutdown. |
| 🗃️ | Typed settings | Pluggable codecs for action settings, global settings, and Property Inspector messages. |
| 🖥️ | Property Inspector | A versioned, dependency-free JavaScript client with connection state, events, settings helpers, and queued startup messages. |
| 🧰 | Plugin services | Start and stop plugin-owned background services in a predictable order. |
| 📦 | Distribution tooling | A CLI resource copier, PyInstaller example, package verification, CI, and Trusted Publishing workflow. |
| 🛡️ | Forward compatibility | Unknown but valid events can be preserved as UnknownStreamDockEvent instead of breaking the plugin. |
flowchart LR
App["MiraBox Stream Dock<br>Windows"] <-->|"WebSocket · JSON"| Boundary["Typed Stream Dock boundary"]
Boundary --> Runtime["StreamDockRuntime<br>keyed-serial dispatcher"]
Registry["ActionRegistry<br>UUID → Action class"] --> Runtime
Runtime --> Actions["Action instances<br>one per context"]
PI["Property Inspector<br>HTML / JavaScript"] <-->|"settings and messages"| App
Client["MiraBoxPropertyInspector<br>browser client"] --> PI
Stream Dock starts the packaged plugin executable with the WebSocket port,
plugin UUID, registration event, and application metadata. run_plugin_cli()
parses those arguments, while create_stream_dock_application() composes the
typed boundary and runtime that register the plugin and route incoming events.
- Python
3.11+; - MiraBox Stream Dock
2.10.179.426or newer (declared minimum); websocket-client>=1.8,<2(installed automatically);- Windows to run Stream Dock and package a standalone plugin with PyInstaller.
The SDK's Stream Dock integration has been manually verified with Stream Dock
3.10.203.0701.
The SDK itself and its test suite can be developed on Windows, Linux, or WSL.
The final .exe must be built on Windows because PyInstaller is not a
cross-compiler.
Install the released package from PyPI:
python -m pip install mirabox-stream-dock-sdkTo work on the SDK from source:
git clone https://github.com/Nekit678/MiraboxStreamDockSDK.git
cd MiraboxStreamDockSDK
python -m venv .venvWindows PowerShell
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"Linux / WSL
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"Define dependencies shared by your action instances, register each action UUID,
and return a configured StreamDockApplication from the application factory:
from __future__ import annotations
from dataclasses import dataclass
from mirabox_sdk import (
Action,
ActionRegistry,
JsonObject,
KeyDownEvent,
PluginLaunchArguments,
StreamDockApplication,
StreamDockSender,
WillAppearEvent,
create_stream_dock_application,
run_plugin_cli,
)
ACTION_UUID = "com.example.counter.increment"
@dataclass(frozen=True, slots=True)
class Dependencies:
stream_dock: StreamDockSender
registry: ActionRegistry[Dependencies] = ActionRegistry()
@registry.register(ACTION_UUID)
class CounterAction(Action[JsonObject, Dependencies]):
def _render(self) -> None:
count = self.settings.get("count", 0)
self.set_title(str(count if type(count) is int else 0))
def on_will_appear(self, _event: WillAppearEvent) -> None:
self._render()
def on_key_down(self, _event: KeyDownEvent) -> None:
count = self.settings.get("count", 0)
self.set_settings({"count": (count if type(count) is int else 0) + 1})
self._render()
def build_application(arguments: PluginLaunchArguments) -> StreamDockApplication:
return create_stream_dock_application(
arguments,
action_factory=registry,
action_dependencies_factory=Dependencies,
)
if __name__ == "__main__":
raise SystemExit(run_plugin_cli(build_application))The exact same action UUID must appear in the plugin's manifest.json. Stream
Dock creates and removes action contexts through willAppear and
willDisappear; the runtime manages the corresponding Python instances.
Override only the callbacks an action needs:
| Input or lifecycle | Action callback |
|---|---|
| Action becomes visible or disappears | on_will_appear, on_will_disappear |
| Key press or release | on_key_down, on_key_up |
| Touch panel tap | on_touch_tap |
| Dial press, release, or rotation | on_dial_down, on_dial_up, on_dial_rotate |
| Settings or title parameters change | on_did_receive_settings, on_title_parameters_did_change |
| Property Inspector opens, closes, or sends data | on_property_inspector_did_appear, on_property_inspector_did_disappear, on_send_to_plugin |
| Device, application, and wake-up notifications | on_device_did_connect, on_device_did_disconnect, on_application_did_launch, on_application_did_terminate, on_system_did_wake_up |
Action helper methods cover the common outbound commands: set_title(),
set_image(), set_state(), set_settings(), get_settings(), show_ok(),
show_alert(), open_url(), log_message(), and
send_to_property_inspector(). Display-only updates also have non-blocking
set_title_async(), set_image_async(), and set_state_async() variants.
Actions use JSON objects by default. To work with an application-specific type,
provide a JsonCodec on the action class:
from dataclasses import dataclass
from mirabox_sdk import Action, FunctionalJsonCodec, JsonObject
@dataclass(frozen=True, slots=True)
class CounterSettings:
count: int
def decode_settings(value: JsonObject) -> CounterSettings:
count = value.get("count", 0)
if type(count) is not int:
raise ValueError("count must be an integer")
return CounterSettings(count)
COUNTER_SETTINGS_CODEC = FunctionalJsonCodec(
decoder=decode_settings,
encoder=lambda value: {"count": value.count},
)
class CounterAction(Action[CounterSettings, Dependencies]):
settings_codec = COUNTER_SETTINGS_CODECThe codec boundary verifies that encoded values are valid JSON. Decode errors are wrapped with the relevant event name and settings path.
application.global_settings is the one runtime-owned facade shared by the
application and context-aware action-dependency and service factories. Use
update() when several in-memory changes belong to one logical operation. The
callback works on an isolated draft; an exception or invalid JSON result rolls
back the complete update:
def append_items(settings: JsonObject) -> None:
items = settings.get("items")
if not isinstance(items, list):
raise ValueError("items must be a list")
items.extend(values)
application.global_settings.update(append_items)After the callback succeeds, the transaction validates the complete draft and
persists it with one setGlobalSettings command. Callback, validation, and send
failures leave the previous local state unchanged. snapshot() returns an
isolated copy, so changing it never changes the runtime state. Use set() or
set_typed() for complete replacements.
Copy the JavaScript client shipped with the installed SDK into the plugin bundle:
mirabox-sdk copy-property-inspector \
com.example.counter.sdPlugin/property-inspectorThe command refuses to overwrite a different copy by default. Pass --force
when intentionally updating the bundled client.
Load it before the action-specific script:
<script src="mirabox-sdk.js"></script>
<script src="counter.js"></script>Stream Dock invokes the compatibility callback automatically. The action script
uses the shared client through window.MiraBoxPropertyInspector:
const client = window.MiraBoxPropertyInspector;
client.on("connected", ({ settings }) => {
console.log("Current settings", settings);
});
client.on("didReceiveSettings", ({ payload }) => {
console.log("Updated settings", payload.settings);
});
client.sendToPlugin({ event: "refresh" });
client.updateSettings({ mode: "toggle" });The client exposes on(), off(), send(), sendToPlugin(), setSettings(),
updateSettings(), and getSettings(), plus connection and registration state.
Messages sent while the WebSocket is connecting are queued until it opens.
examples/counter_plugin
is a complete plugin rather
than an isolated code fragment. It includes:
- a package with a registered counter action;
- a Property Inspector that can reset the counter;
- a valid
.sdPluginbundle and manifest; - SVG assets and a PyInstaller specification;
- tests for the plugin behavior.
Build its executable on Windows:
python -m pip install pyinstaller
python -m PyInstaller --clean --noconfirm examples/counter_plugin/build.spec
Copy-Item dist\CounterPlugin.exe `
examples\counter_plugin\com.example.counter.sdPlugin\Copy the resulting com.example.counter.sdPlugin directory to
%APPDATA%\HotSpot\StreamDock\plugins\ and restart Stream Dock. See the
example guide
for a source-run command and
the complete packaging flow.
This package is an independent, typed Python implementation of the WebSocket / JSON plugin API published by MiraBox. The primary upstream sources are:
- the official StreamDock Plugin SDK repository, including its Python template;
- the official registration procedure, received events, and events sent reference;
- the official
manifest.jsonreference and Property Inspector guide; - the upstream template overview on DeepWiki for secondary, generated explanations of the repository;
- the Space Platform for publishing completed Stream Dock plugins.
The local protocol map connects each supported wire event and command to its Python model or helper and calls out behavior verified in Stream Dock but not currently listed in the upstream event reference. When the published documentation and observed runtime behavior differ, tests record the behavior implemented by this SDK.
| Area | Public API |
|---|---|
| Runtime | StreamDockApplication, ApplicationRuntime, StreamDockRuntime, ApplicationContext, ApplicationService, SessionReadiness, create_stream_dock_application, RuntimeDispatcherConfig, runtime metrics and ports |
| Actions | Action, ActionRegistry, StreamDockSender |
| Launch and registration | PluginLaunchArguments, registration dataclasses, parse_plugin_cli_arguments, run_plugin_cli |
| Input events | Typed immutable event models and InboundOverflowPolicy |
| Output commands | Registration, settings, title, image, state, feedback, URL, log, and Property Inspector command models; ValidatedWireMessage |
| Application data | JsonCodec, FunctionalJsonCodec, JsonObjectCodec, ValidatedJsonObject, OwnedJsonPayload, typed encode/decode helpers |
| Resources | copy_property_inspector_client, property_inspector_client_bytes, mirabox-sdk CLI |
| Parsing | parse_stream_dock_event, parse_registration_info, typed protocol errors |
| Logging | configure_logging with isolated console, file, and disable controls |
The supported public surface is exported from mirabox_sdk. Objects from
individual modules should be treated as implementation details unless they are
also exported there.
Supported imports are mirabox_sdk and, for the documented runtime namespace,
mirabox_sdk.runtime. In the breaking 0.5.0 release, the unsupported direct
legacy modules (connection, inbound, outbound, plugin, and stores) and
the temporary mirabox_sdk._next namespace were removed from distributions.
ApplicationRuntime is the complete contract used by
StreamDockApplication.runtime; RuntimeLifecycle is deliberately narrower
and cannot be passed to StreamDockApplication. Advanced composition points
accepted by create_stream_dock_application()—ActionFactory,
DependencyAwareActionRegistry, HandlerSchedulerFactory, and
WebSocketConnectorFactory—are supported top-level imports. Do not import
from mirabox_sdk._internal; that namespace is internal.
Extend the quick-start factory with ApplicationService implementations for
repositories, HTTP clients, background workers, and other resources required
by action callbacks:
from dataclasses import dataclass
from mirabox_sdk import (
ApplicationContext,
GlobalSettings,
LogMessageCommand,
PluginLaunchArguments,
StreamDockApplication,
StreamDockSender,
create_stream_dock_application,
)
class Repository:
def start(self) -> None:
...
def stop(self) -> None:
...
@dataclass(frozen=True, slots=True)
class Dependencies:
stream_dock: StreamDockSender
global_settings: GlobalSettings
def build_dependencies(context: ApplicationContext) -> Dependencies:
return Dependencies(
stream_dock=context.stream_dock,
global_settings=context.global_settings,
)
def build_repository(_context: ApplicationContext) -> Repository:
return Repository()
def build_application(arguments: PluginLaunchArguments) -> StreamDockApplication:
return create_stream_dock_application(
arguments,
action_factory=registry,
action_dependencies_factory=build_dependencies,
service_factories=(build_repository,),
)ApplicationContext is immutable and shared by all dependency and service
factories. It provides the same stream_dock, global_settings, and
session_readiness objects to each collaborator, without mutable wiring or
internal imports.
Services start in declaration order before the WebSocket runtime connects and
stop in reverse order after it finishes. If startup fails, only services that
started successfully are stopped. Cleanup always attempts every started
service; a primary startup or runtime failure is preserved. stop() may still
be called from an action callback: service cleanup runs on the application
lifecycle thread before run() returns.
ApplicationService.start() is a process-start phase only. The outbound writer
is not running there, so services must not call StreamDockSender, synchronous
global-settings setters, or block on context.session_readiness.wait(). Those
command calls fail immediately with OutboundCommandBusNotReadyError. A service
that needs Stream Dock I/O should start its own worker, have that worker wait on
context.session_readiness, and send only when wait() returns True:
def run_session_work(context: ApplicationContext) -> None:
if context.session_readiness.wait():
context.stream_dock.send(LogMessageCommand("session is ready"))The readiness signal opens after the transport connects and the registration
and initial global-settings-request commands complete. It does not wait for the
first global-settings response; use context.global_settings.loaded for that
separate state. If session initialization fails or ends before readiness,
wait() returns False; inspect failure to distinguish an initialization
error from normal closure.
Subclass Plugin to observe plugin-wide broadcast events independently of
active actions. Pass the instance as plugin= to
create_stream_dock_application():
from mirabox_sdk import Plugin, SystemDidWakeUpEvent
class MyPlugin(Plugin):
def on_system_did_wake_up(self, event: SystemDidWakeUpEvent) -> None:
refresh_shared_state()The runtime invokes the plugin callback first, then the stable snapshot of
active action callbacks. Global-settings state is updated before either
callback and each recipient receives an isolated settings event. A plugin
callback failure is logged and reported in runtime metrics/result policy, but
does not prevent active actions from receiving the event. PluginHooks remains
supported for its legacy on_unhandled_event()-only contract.
| Exception | Meaning |
|---|---|
InvalidPluginLaunchArgumentsError |
Stream Dock did not provide valid executable arguments. |
InvalidRegistrationInfoError |
The registration metadata JSON has an invalid field. |
MalformedEventError / InvalidFieldError |
A known event is malformed; the error includes its JSON path. |
UnsupportedEventError |
An unknown event was parsed with allow_unknown=False. |
JsonCodecDecodeError |
Plugin-owned settings or messages could not be decoded. |
JsonCodecEncodeError |
A codec produced a value that cannot be sent as JSON. |
OutboundQueueFullError |
The bounded outbound command queue is full. |
OutboundCommandBusNotReadyError |
A command was submitted before the outbound writer started. |
OutboundCommandBusClosedError |
A command was submitted after outbound shutdown began. |
By default, parse_stream_dock_event() preserves an unknown but structurally
valid envelope as UnknownStreamDockEvent. This lets the SDK tolerate protocol
extensions while known events remain strictly validated. The runtime delivers
each preserved event once to the configured Plugin or legacy PluginHooks
object. Unknown envelopes are not broadcast to actions because their routing
semantics are not known yet.
Protocol parsing metadata and runtime routing metadata are maintained in separate validated internal registries. The public API exposes typed event models rather than registry implementation details.
The typed boundary parses frames outside application callbacks and puts valid
events into a bounded queue. A keyed-serial worker pool invokes action
callbacks: one action context remains strictly ordered, while different
contexts can make progress concurrently. willAppear, willDisappear,
broadcast, and unknown events are exclusive ordering barriers; each waits for
earlier context callbacks and completes before later callbacks start. On normal
shutdown, the queue drains before StreamDockApplication.run() returns and
before the runtime releases actions.
The pool defaults to four workers and the queue defaults to 1,024 events.
Lifecycle, settings, input, broadcast, unknown, and every other event except
dialRotate are lossless by default. dialRotate is explicitly coalescable
and may be discarded on overflow. Configure worker concurrency, the limit, and
overflow behavior for discardable events when constructing the application:
from mirabox_sdk import (
InboundOverflowPolicy,
RuntimeDispatcherConfig,
StreamDockQueueConfig,
create_stream_dock_application,
)
application = create_stream_dock_application(
arguments,
action_factory=registry,
action_dependencies_factory=Dependencies,
queue_config=StreamDockQueueConfig(
raw_inbound_limit=512,
inbound_event_limit=512,
outbound_command_limit=512,
raw_outbound_limit=512,
session_event_limit=16,
),
runtime_config=RuntimeDispatcherConfig(worker_count=4),
inbound_overflow_policy=InboundOverflowPolicy.DROP_OLDEST,
coalesce_dial_rotations=True,
)DROP_NEWEST (the default) discards the newest eligible dialRotate;
DROP_OLDEST discards the oldest eligible rotation. Neither policy may evict a
lossless event. If the queue contains only lossless events, another lossless
event applies backpressure to the WebSocket reader until the dispatcher frees
space; an incoming rotation is discarded instead. This keeps memory bounded
without allowing overflow to corrupt runtime state.
Rotation coalescing is opt-in: compatible pending dialRotate events for the
same context and pressed state are combined by summing ticks; an intervening
event for that context, or any broadcast/unknown event, prevents coalescing.
Read application.metrics() for immutable queue, event-pump, scheduler, route,
action, session, and transport snapshots. Runtime and boundary shutdown stages
are bounded to five seconds by default; pass None explicitly only when an
unbounded wait is required. At a timeout, pending and active work remains
observable in metrics and metadata-only diagnostics.
Python cannot safely stop a running thread. A callback that exceeds the timeout
continues on its daemon worker until the callback itself returns, even though
close() proceeds. Callback code should therefore use its own bounded I/O and
cooperative cancellation where appropriate.
Every StreamDockApplication owns one dedicated outbound writer.
Calling send() puts the typed command into a bounded FIFO queue; only that
writer validates and serializes the command, emits its protocol log, and calls
the WebSocket transport. Concurrent plugin threads therefore cannot interleave
frames. send() waits for its command's result, so serialization and transport
errors still reach the caller and state-update helpers retain their rollback
behavior.
send_async() performs the same queue acceptance but returns a
CommandFuture before serialization or WebSocket I/O. Queue-full, pre-start,
and shutdown rejections are raised immediately; call future.result() only
when the eventual writer-side error or completion matters. Before run() has
started the command writer, both send() and send_async() raise
OutboundCommandBusNotReadyError and do not enqueue a command. For
high-frequency display rendering, Action.set_image_async(),
set_title_async(), and set_state_async() avoid holding an inbound callback
while the writer is slow. Rollback-sensitive settings helpers remain
synchronous.
The outbound queue holds 1,024 waiting commands by default. It never silently
drops a command when full: send() and send_async() raise
OutboundQueueFullError. Configure its capacity together with the other
boundary limits:
from mirabox_sdk import StreamDockQueueConfig, create_stream_dock_application
application = create_stream_dock_application(
arguments,
action_factory=registry,
action_dependencies_factory=Dependencies,
queue_config=StreamDockQueueConfig(
raw_inbound_limit=512,
inbound_event_limit=512,
outbound_command_limit=512,
raw_outbound_limit=512,
session_event_limit=16,
),
coalesce_commands=True,
)Coalescing is opt-in. Compatible adjacent pending setState, setTitle,
setImage, setSettings, or setGlobalSettings commands for the same
semantic target are replaced by their newest value. Commands of another type or
target are ordering barriers. All callers whose commands were combined receive
distinct CommandFuture handles backed by the queued command's single
completion state, and therefore observe the same final write result. The queue
retains at most one completion state per physical entry.
Read application.metrics().boundary for atomic outbound queue, writer, raw
transport, and connector snapshots. Before writer startup, submissions raise
OutboundCommandBusNotReadyError; once outbound command shutdown starts, they
raise OutboundCommandBusClosedError. Accepted commands receive exactly one
terminal result through the canonical CommandFuture.
The runtime uses explicit thread ownership:
| Surface | Supported caller or owner |
|---|---|
configure_logging() and StreamDockApplication.run() / stop() |
Application lifecycle thread; configure logging before run(); stop() is idempotent and may also be called concurrently |
| WebSocket frame I/O and typed protocol parsing | Boundary-owned transport/codec workers |
Every Action, Plugin, and PluginHooks callback |
Runtime-owned keyed workers; callbacks are serial per context and may overlap across contexts, while lifecycle, broadcast, and unknown barriers run exclusively |
StreamDockSender.send() / send_async() and action command helpers |
Any application, service, or action-callback thread after the outbound writer starts; overlapping calls are supported |
StreamDockApplication.stop() |
Any application or action-callback thread; calls are idempotent and may overlap |
The outbound queue establishes FIFO order when it accepts commands. Calls that
do not overlap retain caller order; the relative order of simultaneous calls
is intentionally unspecified. Each send() waits only for its own accepted
submission (or the final coalesced write) and receives its serialization,
transport, overflow, or shutdown result. send_async() returns after
acceptance; the returned CommandFuture exposes the later result.
Scalar-only frozen command objects may be shared between threads.
Payload-bearing commands own mutable OwnedJsonPayload data: do not mutate a
command or its payload once any thread begins send() or send_async().
ValidatedJsonObject backing snapshots are safe to hand between threads after
construction, but every mutable COW view—event settings, Action.settings, and
OwnedJsonPayload—allows only one accessing or mutating thread at a time.
application.global_settings.snapshot() is isolated, and its update() method
serializes rollback-safe changes from background services; do not share a live
mutable view between threads.
Shutdown closes the typed boundary, drains owned inbound work while callback
commands can still finish, stops the scheduler and pumps, and finally releases
all action contexts. A background thread that needs to interrupt run() calls
application.stop().
SDK logging is disabled by default: it does not propagate to the application's
root logger and does not create a log file. Enable diagnostics explicitly before
calling run_plugin_cli():
from mirabox_sdk import configure_logging
configure_logging(level="INFO")When enabled without a file, the destination is stderr. To write UTF-8 logs to a file, pass a path; missing parent directories are created automatically. File logging rotates at 5 MiB with three backups by default:
from pathlib import Path
from mirabox_sdk import LoggingOverflowPolicy, configure_logging
configure_logging(
level="DEBUG",
log_file=Path.home() / ".mirabox-counter" / "plugin.log",
max_bytes=5 * 1024 * 1024,
backup_count=3,
logging_queue_limit=1024,
logging_overflow_policy=LoggingOverflowPolicy.DROP_NEWEST,
)Adjust max_bytes and backup_count for the plugin's needs. Set
max_bytes=0 only when intentionally requesting an unbounded file.
logging_queue_limit bounds the number of records waiting for the managed
listener; it must be positive. DROP_NEWEST preserves already queued records,
while DROP_OLDEST keeps the most recent records of the same priority.
include_payload=True adds the complete inbound and outbound protocol message
to DEBUG records. Payloads may contain tokens, settings, and other secrets, so
enable this option only temporarily in a trusted development environment. Omit
the option (its default is False) to return to redacted payloads while keeping
other diagnostics enabled.
configure_logging(
level="DEBUG",
log_file=Path.home() / ".mirabox-counter" / "plugin.log",
include_payload=True,
)Repeated calls replace the handler previously installed by
configure_logging(), draining its queue first, so the level or destination can
be changed without duplicating messages. Return the SDK to its default silent
state and flush pending records with:
configure_logging(enabled=False)INFO records cover connection lifecycle and operational status. Per-message
protocol direction, event, and context are emitted only at DEBUG. Message
payloads remain redacted unless include_payload=True is explicitly configured.
SDK records are handed to one managed logging thread, so stream and rotating
file I/O never runs in the WebSocket reader, inbound dispatcher, outbound
writer, or calling service thread. Handlers installed manually by the
application remain its responsibility and are outside that guarantee. The
managed queue is bounded and producers never wait for destination I/O. On
overflow, an ERROR or CRITICAL record displaces a lower-level record when
possible and is processed ahead of queued DEBUG through WARNING records.
Lower-level records never displace queued errors; within the same priority
class, the configured overflow policy is applied. dropped_log_records()
returns the process-wide, thread-safe count of records discarded by all managed
logging queues:
from mirabox_sdk import dropped_log_records
if dropped_log_records():
# The destination has not kept up with the configured log volume.
...MiraboxStreamDockSDK/
├── pyproject.toml # Package metadata and tool configuration
├── src/mirabox_sdk/
│ ├── action.py # Reusable action base class
│ ├── action_registry.py # Action UUID registry
│ ├── completion.py # Canonical command completion contract
│ ├── commands.py # Typed outbound commands
│ ├── events.py # Typed inbound event models
│ ├── parser.py # Strict wire-message parser
│ ├── runtime/ # Stable application/runtime API
│ ├── _internal/ # Private boundary/dispatcher implementation
│ ├── logging_config.py # Isolated SDK logging configuration
│ └── property_inspector/ # Browser-side SDK resource
├── examples/counter_plugin/ # Complete buildable plugin
├── tests/ # SDK and release-tool tests
├── scripts/ # Version and distribution verification
└── .github/workflows/ # CI and Trusted Publishing release jobs
Install the development dependencies, then run the same checks as CI:
python -m unittest discover -s tests -v
PYTHONPATH=examples/counter_plugin/src \
python -m unittest discover -s examples/counter_plugin/tests -v
python -m compileall -q src tests scripts examples
ruff check src tests scripts examples
ruff format --check src tests scripts examples
python -m build
python scripts/verify_distribution.py dist
python -m twine check dist/*The test suite uses fake connections and protocol messages; it does not require a running Stream Dock instance. CI runs the SDK on Linux and Windows across all supported Python versions.
Contributions are welcome. Please read CONTRIBUTING.md before submitting a change, and include the Stream Dock version and a regression test when changing observed protocol behavior.
Releases are built from version tags, published to PyPI through Trusted Publishing, and attached to a generated GitHub Release. The required one-time configuration and release checklist are documented in RELEASING.md.
Distributed under the MIT License.