Repository navigation
python-client: package as scrypted-sdk for PyPI - #2095
Merged
Merged
Conversation
plugin_remote now imports cluster_labels, cluster_setup, plugin_console,
plugin_pip, and plugin_volume (plus a lazy plugin_repl import), but
packages/python-client only symlinks plugin_remote, rpc, rpc_reader, and
scrypted_python. As a result the client cannot be imported at all:
ModuleNotFoundError: No module named 'cluster_labels'
Add symlinks for the missing modules, matching the existing pattern of
sharing one implementation with server/python.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WaSWgBV1XsZxWiYM16sWME
The only way to connect to Scrypted from Python has been to copy the bootstrap out of test.py, which is not importable (module-level event loop) and had drifted from the current rpc_reader/PluginRemote APIs (writeJSON vs writeSerialized, missing ClusterSetup argument). Move the transport and connection handshake into an importable scrypted_client module: - EioRpcTransport gains a close() method, an optional injectable aiohttp session for the engine.io connection, and queues its send loop on the running loop instead of via run_coroutine_threadsafe. - connect_scrypted_client() performs login, the engine.io connect, and the getRemote handshake, with a connect timeout and consistent ScryptedConnectionError on failure. It also attaches the constructed SystemManager to remote.systemManager, the same wiring loadZip does for plugins, so PluginRemote.notify can dispatch events to systemManager.listen() callbacks. - test.py becomes a small demo of the library and exits cleanly without os._exit(); the server URL is configurable via SCRYPTED_BASE_URL. Verified live against a Scrypted server (device enumeration and OnOff state reads). Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WaSWgBV1XsZxWiYM16sWME
Address review feedback: - close() now cancels and awaits both the send loop and the peer read loop (previously the send task was cancelled but never awaited and the read task was untracked), so closing the event loop after close() no longer risks 'Task was destroyed but it is pending' warnings. - connect_scrypted_client() stores the read-loop task on the transport and propagates read-loop failures into the pending handshake future, so a link that dies mid-handshake fails immediately with the underlying error instead of waiting out the timeout. - The timeout parameter now bounds every phase: the login POST (aiohttp ClientTimeout), the engine.io connect (asyncio.wait_for), and the wait for initial system state. - Document session ownership: login_session is borrowed and never closed; an http_session given to EioRpcTransport is owned by the transport and closed by close(). Verified live against a Scrypted server: clean run with empty stderr, plus connection-refused and bad-credential paths both raising ScryptedConnectionError. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WaSWgBV1XsZxWiYM16sWME
raman325
marked this pull request as draft
July 16, 2026 17:57
A client consuming plugin_remote (for SystemManager, DeviceManager, MediaManager) only needs the types and the engine.io/rpc bits. The plugin host modules (cluster_labels, plugin_console, plugin_pip, plugin_volume) are only used inside loadZipWrapped, so import them there -- importing plugin_remote no longer requires them, and the client directory drops those symlinks (plugin_repl was already a lazy import). cluster_setup stays: the client bootstrap constructs a ClusterSetup. Co-Authored-By: Claude Fable 5 <[email protected]>
Adds a pyproject.toml and a hatchling build hook that generates the scrypted_sdk package at build time: it copies the client-required modules (client.py, rpc, rpc_reader, cluster_setup, plugin_remote, and the scrypted_python types -- reading through the symlinks into server/python and sdk/types) and mechanically rewrites the flat imports to package-qualified ones (import rpc -> from scrypted_sdk import rpc), so nothing is installed into consumers' top-level namespace. No runtime code is modified and nothing generated is committed. A python-sdk-v* tag builds, smoke-tests, and publishes to the existing scrypted-sdk PyPI project via trusted publishing; the same workflow builds and smoke-tests on PRs that touch the shared Python sources. Co-Authored-By: Claude Fable 5 <[email protected]>
…example The Python equivalent of packages/client/examples/light.ts: connect, look up a light by name, turn it on, wait, turn it off, disconnect. Works both with the installed scrypted-sdk package and directly from a repo checkout (falls back to the flat modules in the parent directory). Co-Authored-By: Claude Fable 5 <[email protected]>
raman325
force-pushed
the
python-client-pypi
branch
from
July 16, 2026 18:34
bd74e38 to
cab5651
Compare
raman325
marked this pull request as ready for review
July 27, 2026 20:40
raman325
added a commit
to raman325/ha_scrypted
that referenced
this pull request
Jul 31, 2026
scrypted-sdk 0.1.0 is now on PyPI (koush/scrypted#2095), so: - drop vendor/ symlink farm and the sdk_compat sys.path shim - declare scrypted-sdk==0.1.0 in manifest.json requirements - rewrite rpc_transport.py as thin HA glue over scrypted_sdk.connect_scrypted_client (which upstreamed the transport, login flow, and plugin-remote handshake, adding per-phase timeouts and handshake failure detection); only HA session wiring remains local - import types from scrypted_sdk.scrypted_python.scrypted_sdk.types Co-Authored-By: Claude Fable 5 <[email protected]>
Recoveredd
pushed a commit
to Recoveredd/scrypted
that referenced
this pull request
Sep 28, 2026
* python-client: link transitive server modules required by plugin_remote
plugin_remote now imports cluster_labels, cluster_setup, plugin_console,
plugin_pip, and plugin_volume (plus a lazy plugin_repl import), but
packages/python-client only symlinks plugin_remote, rpc, rpc_reader, and
scrypted_python. As a result the client cannot be imported at all:
ModuleNotFoundError: No module named 'cluster_labels'
Add symlinks for the missing modules, matching the existing pattern of
sharing one implementation with server/python.
* python-client: extract reusable scrypted_client library from test.py
The only way to connect to Scrypted from Python has been to copy the
bootstrap out of test.py, which is not importable (module-level event
loop) and had drifted from the current rpc_reader/PluginRemote APIs
(writeJSON vs writeSerialized, missing ClusterSetup argument).
Move the transport and connection handshake into an importable
scrypted_client module:
- EioRpcTransport gains a close() method, an optional injectable
aiohttp session for the engine.io connection, and queues its send
loop on the running loop instead of via run_coroutine_threadsafe.
- connect_scrypted_client() performs login, the engine.io connect, and
the getRemote handshake, with a connect timeout and consistent
ScryptedConnectionError on failure. It also attaches the constructed
SystemManager to remote.systemManager, the same wiring loadZip does
for plugins, so PluginRemote.notify can dispatch events to
systemManager.listen() callbacks.
- test.py becomes a small demo of the library and exits cleanly
without os._exit(); the server URL is configurable via
SCRYPTED_BASE_URL.
Verified live against a Scrypted server (device enumeration and OnOff
state reads).
* python-client: bound all connect phases and tear down tasks on close
Address review feedback:
- close() now cancels and awaits both the send loop and the peer read
loop (previously the send task was cancelled but never awaited and
the read task was untracked), so closing the event loop after
close() no longer risks 'Task was destroyed but it is pending'
warnings.
- connect_scrypted_client() stores the read-loop task on the transport
and propagates read-loop failures into the pending handshake future,
so a link that dies mid-handshake fails immediately with the
underlying error instead of waiting out the timeout.
- The timeout parameter now bounds every phase: the login POST
(aiohttp ClientTimeout), the engine.io connect (asyncio.wait_for),
and the wait for initial system state.
- Document session ownership: login_session is borrowed and never
closed; an http_session given to EioRpcTransport is owned by the
transport and closed by close().
Verified live against a Scrypted server: clean run with empty stderr,
plus connection-refused and bad-credential paths both raising
ScryptedConnectionError.
* python-client: lazily import plugin host modules in plugin_remote
A client consuming plugin_remote (for SystemManager, DeviceManager,
MediaManager) only needs the types and the engine.io/rpc bits. The
plugin host modules (cluster_labels, plugin_console, plugin_pip,
plugin_volume) are only used inside loadZipWrapped, so import them
there -- importing plugin_remote no longer requires them, and the
client directory drops those symlinks (plugin_repl was already a lazy
import). cluster_setup stays: the client bootstrap constructs a
ClusterSetup.
* python-client: package as scrypted-sdk for PyPI
Adds a pyproject.toml and a hatchling build hook that generates the
scrypted_sdk package at build time: it copies the client-required
modules (client.py, rpc, rpc_reader, cluster_setup, plugin_remote, and
the scrypted_python types -- reading through the symlinks into
server/python and sdk/types) and mechanically rewrites the flat imports
to package-qualified ones (import rpc -> from scrypted_sdk import rpc),
so nothing is installed into consumers' top-level namespace. No runtime
code is modified and nothing generated is committed.
A python-sdk-v* tag builds, smoke-tests, and publishes to the existing
scrypted-sdk PyPI project via trusted publishing; the same workflow
builds and smoke-tests on PRs that touch the shared Python sources.
* python-client: add examples/light.py mirroring the typescript client example
The Python equivalent of packages/client/examples/light.ts: connect,
look up a light by name, turn it on, wait, turn it off, disconnect.
Works both with the installed scrypted-sdk package and directly from a
repo checkout (falls back to the flat modules in the parent directory).
---------
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Makes
packages/python-clientpublishable to the existing scrypted-sdk PyPI project. Three pieces, all insidepackages/python-clientexcept the workflow:pyproject.toml—scrypted-sdk0.1.0, hatchling backend, deps mirroringrequirements.txt(aiohttp,aiodns,python-engineio[asyncio_client]).hatch_build.py— a build hook that generates thescrypted_sdkpackage at build time. The modules in this directory are flat top-level modules (that's how the plugin runtime loads them), so publishing them as-is would install modules namedrpc,plugin_remote, etc. into consumers' environments. The hook copies each module (reading through the symlinks, which also solves symlinks-don't-ship) and mechanically rewrites the flat imports:import rpcfrom scrypted_sdk import rpcimport plugin_volume as pvfrom scrypted_sdk import plugin_volume as pvfrom cluster_setup import ClusterSetupfrom scrypted_sdk.cluster_setup import ClusterSetupimport scrypted_python.scrypted_sdk.typesfrom scrypted_sdk import scrypted_python; import scrypted_sdk.scrypted_python.scrypted_sdk.typesNothing else is touched — the dynamic plugin-zip imports inside
loadZip(from scrypted_sdk import sdk_init2,from main import ...) are deliberately left alone, and nothing generated is committed (.build/is gitignored). Consumers dofrom scrypted_sdk import connect_scrypted_client..github/workflows/python-sdk.yml— builds + smoke-tests (installs the wheel in a clean venv and imports it) on PRs touchingpackages/python-client,server/python, orsdk/types/scrypted_python; on apython-sdk-v*tag it additionally publishes to PyPI via trusted publishing (OIDC, no API tokens to store).What you'd need to do (one-time, ~2 min)
On https://pypi.org/manage/project/scrypted-sdk/settings/publishing/ add a trusted publisher: owner
koush, reposcrypted, workflowpython-sdk.yml, environmentpypi. After that, releasing is: bumpversioninpyproject.toml, push apython-sdk-v0.1.0tag.Vendors only client-required modules
Per review feedback, the wheel ships just the types and the engine.io/rpc bits:
client.py,rpc,rpc_reader,cluster_setup,plugin_remote, and thescrypted_pythontypes. The enabling change (plugin_remotelazily importing the plugin host modules insideloadZipWrapped) lives in #2094.Stacked on #2094
This branch includes #2094's commits (the packaged public API is its
scrypted_client.py); merge that first and this diff reduces to the packaging commit (02f7c2d) and the example (cab5651).Testing
uv buildproduces sdist + wheel; the wheel is built from the sdist, so the sdist round-trip is exercised.import scrypted_sdkworks, all public API names resolve, andimportlib.util.find_specconfirmsrpc/plugin_remote/cluster_setup/scrypted_pythondo not exist at the top level.🤖 Generated with Claude Code
Example
examples/light.pyis the Python equivalent ofpackages/client/examples/light.ts: connect,getDeviceByName,turnOn, wait,turnOff, disconnect.SCRYPTED_USERNAME=admin SCRYPTED_PASSWORD=swordfish python examples/light.py "Office Dimmer"It runs both against the installed
scrypted-sdkpackage and directly from a repo checkout with onlyrequirements.txtinstalled (it falls back to the flat modules in the parent directory). Both paths verified: each reaches the server and surfaces a cleanScryptedConnectionErrorwhen pointed at a dead host.