Skip to content

python-client: package as scrypted-sdk for PyPI - #2095

Merged
koush merged 8 commits into
koush:mainfrom
raman325:python-client-pypi
Jul 27, 2026
Merged

koush merged 8 commits into
koush:mainfrom
raman325:python-client-pypi

Conversation

@raman325

@raman325 raman325 commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

What

Makes packages/python-client publishable to the existing scrypted-sdk PyPI project. Three pieces, all inside packages/python-client except the workflow:

  1. pyproject.toml — scrypted-sdk 0.1.0, hatchling backend, deps mirroring requirements.txt (aiohttp, aiodns, python-engineio[asyncio_client]).

  2. hatch_build.py — a build hook that generates the scrypted_sdk package 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 named rpc, 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:

    upstream packaged
    import rpc from scrypted_sdk import rpc
    import plugin_volume as pv from scrypted_sdk import plugin_volume as pv
    from cluster_setup import ClusterSetup from scrypted_sdk.cluster_setup import ClusterSetup
    import scrypted_python.scrypted_sdk.types from scrypted_sdk import scrypted_python; import scrypted_sdk.scrypted_python.scrypted_sdk.types

    Nothing 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 do from scrypted_sdk import connect_scrypted_client.

  3. .github/workflows/python-sdk.yml — builds + smoke-tests (installs the wheel in a clean venv and imports it) on PRs touching packages/python-client, server/python, or sdk/types/scrypted_python; on a python-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, repo scrypted, workflow python-sdk.yml, environment pypi. After that, releasing is: bump version in pyproject.toml, push a python-sdk-v0.1.0 tag.

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 the scrypted_python types. The enabling change (plugin_remote lazily importing the plugin host modules inside loadZipWrapped) 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 build produces sdist + wheel; the wheel is built from the sdist, so the sdist round-trip is exercised.
  • Wheel installed into a clean venv: import scrypted_sdk works, all public API names resolve, and importlib.util.find_spec confirms rpc/plugin_remote/cluster_setup/scrypted_python do not exist at the top level.

🤖 Generated with Claude Code

Example

examples/light.py is the Python equivalent of packages/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-sdk package and directly from a repo checkout with only requirements.txt installed (it falls back to the flat modules in the parent directory). Both paths verified: each reaches the server and surfaces a clean ScryptedConnectionError when pointed at a dead host.

raman325 and others added 3 commits July 15, 2026 18:35
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
Copilot AI review requested due to automatic review settings July 16, 2026 17:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@raman325
raman325 marked this pull request as draft July 16, 2026 17:57
raman325 and others added 3 commits July 16, 2026 14:33
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
raman325 marked this pull request as ready for review July 27, 2026 20:40
@koush
koush merged commit 4d2d367 into koush:main Jul 27, 2026
2 checks passed
@raman325
raman325 deleted the python-client-pypi branch July 27, 2026 22:02
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).


---------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants