Skip to main content

SDK and CLI for authoring Owncast plugins in Python

Project description

owncast-plugin-py (Python)

SDK for authoring Owncast plugins in Python. Plugins ship their Python source and run sandboxed inside the Owncast server on an embedded Python engine. They use the same runtime, wire protocol, and .ocpkg format as the JavaScript SDK, so a Python plugin is a first-class peer of a JS one.

You write ordinary Python with decorators. There is no compile step and no PDK to install: the Owncast host embeds one Python engine and runs every Python plugin on it, so your plugin ships as plain source.

Plugins are source, not a compiled binary

Because the host supplies the Python engine, a plugin package contains only your plugin.py, its manifest, and any assets, with no bundled interpreter. That keeps packages small and means an author never installs or runs a wasm toolchain (extism-py, binaryen) at all. Those are a maintainer-only dependency of building the engine itself.

Quick start

Scaffold a new plugin project (the Python peer of npm create owncast-plugin) with no install, straight from the published package:

uvx owncast-plugin-py new my-plugin   # drops a working starter into ./my-plugin

To get the owncast-plugin-py CLI on your PATH for the build/test/serve/package steps, install the SDK. It fetches and caches the host test/serve binaries on first use, so there's nothing else to install by hand. There's no wasm compiler to fetch, because plugins run on the engine the host embeds.

uv tool install owncast-plugin-py      # or:  pip install owncast-plugin-py

This writes a my-plugin/ directory with plugin.manifest.json, src/plugin.py, a sample __tests__/plugin.test.json, and docs (README, INSTRUCTIONS.md, AGENTS.md + a create-owncast-plugin-py skill) already wired up. A plugin is just a directory:

my-plugin/
├── plugin.manifest.json     # name, slug, version, permissions
├── src/plugin.py            # your code
└── __tests__/*.test.json    # optional scenario tests

Build, test, serve, and package it:

owncast-plugin-py build my-plugin      # emit src/plugin.py -> <slug>.py
owncast-plugin-py test my-plugin       # run the __tests__/ scenarios
owncast-plugin-py serve my-plugin      # local dev server (POST /_dev/chat to drive it)
owncast-plugin-py package my-plugin    # build + bundle -> <slug>.ocpkg (the only file you ship)

Install the .ocpkg in Owncast from the admin Plugins page (Upload plugin) or by copying it to the server's data/plugins/ directory, then toggle Enabled.

CI / no-install: the build/package step is also runnable directly with python3 sdks/python/owncast_plugin_build.py <dir> [--package], with no toolchain on PATH (it just emits source). OWNCAST_PLUGIN_HOST_BIN_DIR points test/serve at locally-built host binaries, and OWNCAST_PLUGIN_HOST_BINARIES_VERSION pins the release they're fetched from.

Writing a plugin

Import plugin, owncast, and filter, and register handlers with decorators:

from owncast_plugin import plugin, owncast, filter


@plugin.on_chat_message
def greet(msg):
    owncast.chat.send(f"echo: {msg.body}")


@plugin.filter_chat_message
def block_spam(msg):
    return filter.drop("spam") if "spam" in msg.body else filter.pass_()

Declare the permissions you use (chat.send above) in plugin.manifest.json. The build only wires up the host functions your permissions grant.

Event handlers

Each decorator subscribes to one event, and the SDK derives the manifest subscriptions from which handlers you define.

Decorator Fires on
@plugin.on_chat_message a chat message (notify)
@plugin.filter_chat_message a chat message, before broadcast (return a filter result, requires chat.filter)
@plugin.on_chat_user_joined / _parted / _renamed chat presence
@plugin.on_message_moderated a message hidden/restored
@plugin.on_stream_started / _stopped / _title_changed stream lifecycle
@plugin.on_sse_connect / _disconnect a viewer's SSE stream opened/closed
@plugin.on_tick ~once per second
@plugin.on_fediverse_follow / _like / _repost / _mention / _reply fediverse activity
@plugin.on("custom.event") a plugin-emitted custom event
@plugin.on_tab_content("slug") / @plugin.on_page_content("slug") render dynamic viewer-page HTML
@plugin.on_page_styles / @plugin.on_page_scripts inject viewer-page CSS / JS computed at request time

Payloads are attribute objects with snake_case accessors over the wire JSON (msg.body, msg.user.display_name, msg.client_id). Use msg.raw for the underlying dict.

HTTP routing

Plugins with http.serve can answer requests under /plugins/<slug>/…. Route by path and method declaratively:

@plugin.get("/api/messages")
def list_messages(req):
    return {"status": 200, "body": "...", "headers": {"Content-Type": "application/json"}}

@plugin.post("/api/messages")
def add_message(req):
    body = req.body
    return {"status": 201}

@plugin.on_http_request("/health")   # any method, exact path
def health(req):
    return "ok"                       # a plain string → 200 with that body

@plugin.on_http_request               # bare: catch-all fallback
def fallback(req):
    return {"status": 404}
  • @plugin.get/post/put/delete/patch(path) and @plugin.route(path, methods=[...]) for method-specific routes, and @plugin.on_http_request(path) for any method.
  • Paths are exact and plugin-relative (e.g. /api/messages), excluding the query string. Read query params from req.query.
  • A request whose path matches a route but not its method gets an automatic 405. An unmatched path falls through to the bare catch-all, else 404.
  • A handler returns a dict ({status, body, headers}), a str (→ 200), or None (→ 204).

The owncast host API

owncast.<group>.<method>(...), and each group is gated by the matching manifest permission.

Group Methods
chat send, send_action, system, send_to, reply_to, history, clients, delete_message, kick
kv get, set, get_json, set_json, delete
storage / fs storage.upload, fs.read_text, fs.write, fs.list, fs.delete, fs.exists
server / stream server.info/socials/emotes/federation/tags, stream.current/broadcaster
video_config read, write
notifications discord, browser_push, fediverse
users list, get, set_enabled, ban_ip
events / fediverse / sse events.emit, fediverse.post, sse.send
actions add, clear
timer set_timeout, set_interval, clear
config / assets / http config.get, assets.read_text, http.fetch (needs network.fetch + network.allowedHosts)

Return values that are JSON objects come back as the same attribute objects (owncast.server.info().name), and lists come back as Python lists.

The concepts (events, permissions, the .ocpkg format, the manifest) are shared with the JS SDK, so the Owncast Plugin Author Guide applies. Just read the API names as their Pythonic snake_case forms.

How it works (and how it differs from the JS SDK)

Plugins run on a Python engine the Owncast host embeds and shares across every Python plugin, so there's no per-plugin compile. build writes your src/plugin.py out as <slug>.py, and package zips that with the manifest and assets into the .ocpkg. A single-file plugin is emitted with the from owncast_plugin import … line stripped (the SDK names are already globals in the engine). A plugin that imports other local modules has them inlined into the one shipped plugin.py. You still from owncast_plugin import … for editor support and unit tests.

Consequences worth knowing:

  • The entry can't use relative imports. In src/plugin.py import your own modules absolutely (from helpers import …), not from . import helpers. Relative imports inside a package's own modules are fine.
  • Pure-Python only, no pip. The embedded engine runs pure Python with no filesystem, so there's no pip install and C extensions (numpy, pandas, etc.) won't load. You add a third-party library by copying its pure-Python source into src/ and importing it like any local module. For outbound HTTP use owncast.http.fetch, not requests.
  • Don't shadow stdlib names at module top level. Your code runs in the same global scope as the runtime (which does import json), so a top-level def json(...) in your plugin shadows it and breaks things. Name helpers like json_response instead.
  • snake_case everywhere, vs the JS SDK's camelCase (send_action, get_json, msg.user.display_name, filter.pass_(), where the trailing underscore avoids the Python keyword pass).

Testing

__tests__/*.test.json scenario files are identical in format to the JS SDK's and run through the same owncast-plugin-test binary, so a Python port of a plugin can reuse the JS version's test scenarios verbatim. Each scenario dispatches events / HTTP requests and asserts on observed side effects (chatSends, kv writes, HTTP responses, …).

Status

Working today: the runtime (owncast_plugin/), the owncast-plugin-py CLI (new/build/test/serve/package) with lazy host-binary download, the full host API, HTTP routing, .ocpkg packaging, a pip/uv-installable package (pyproject.toml), and CI that builds + tests every example. All 27 of the JS example plugins have Python counterparts under examples/python/.

Not yet (roadmap): publishing to PyPI and type stubs.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

owncast_plugin_py-0.9.1.tar.gz (37.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

owncast_plugin_py-0.9.1-py3-none-any.whl (40.5 kB view details)

Uploaded Python 3

File details

Details for the file owncast_plugin_py-0.9.1.tar.gz.

File metadata

  • Download URL: owncast_plugin_py-0.9.1.tar.gz
  • Upload date:
  • Size: 37.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for owncast_plugin_py-0.9.1.tar.gz
Algorithm Hash digest
SHA256 680da6cbecb03ca0fe69251290ddaeff0e19f427281a788c18f0b33d4612b83b
MD5 c2d6a56d4eaa06a311449bc117f42df5
BLAKE2b-256 c5e624ca7736beefb7cee0615f97da98110e4c50959e55df940b0d17d3b62611

See more details on using hashes here.

File details

Details for the file owncast_plugin_py-0.9.1-py3-none-any.whl.

File metadata

  • Download URL: owncast_plugin_py-0.9.1-py3-none-any.whl
  • Upload date:
  • Size: 40.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for owncast_plugin_py-0.9.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0229fe1c0fb31e87012ce7616c75bd30b033d257a51b9f17b2b02ffcdb3f2e69
MD5 e0def2950254ae18cc4127748837a43b
BLAKE2b-256 dca8109140bfda5b115865fd544f7300e3643e6f8d2f01c054e27f22a2452546

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page