python-sdk: 0.1.1 — top-level type exports and transport leak fix (#2109)

This commit is contained in:
Raman Gupta
2026-07-31 20:31:14 -04:00
committed by GitHub
parent 0995e98568
commit 84bb20e7eb
4 changed files with 82 additions and 24 deletions

View File

@@ -55,8 +55,20 @@ from scrypted_sdk.client import (
ScryptedConnectionError,
connect_scrypted_client,
)
from scrypted_sdk.plugin_remote import DeviceManager, MediaManager, SystemManager
from scrypted_sdk.plugin_remote import (
DeviceManager,
MediaManager,
PluginRemote,
SystemManager,
)
from scrypted_sdk.scrypted_python.scrypted_sdk import ScryptedStatic
from scrypted_sdk.scrypted_python.scrypted_sdk.types import (
ScryptedDeviceType,
ScryptedInterface,
ScryptedInterfaceMethods,
ScryptedInterfaceProperty,
ScryptedMimeTypes,
)
__all__ = [
"DEFAULT_CONNECT_TIMEOUT",
@@ -64,13 +76,34 @@ __all__ = [
"DeviceManager",
"EioRpcTransport",
"MediaManager",
"PluginRemote",
"ScryptedConnectionError",
"ScryptedDeviceType",
"ScryptedInterface",
"ScryptedInterfaceMethods",
"ScryptedInterfaceProperty",
"ScryptedMimeTypes",
"ScryptedStatic",
"SystemManager",
"connect_scrypted_client",
]
'''
TYPES_PY = '''"""Alias for the full generated type surface.
The concrete type tree lives at scrypted_sdk.scrypted_python.scrypted_sdk.types
(an artifact of build-time package generation); this module exposes all of it
in one place::
from scrypted_sdk.types import ScryptedInterface, VideoClip, LockState
The package root re-exports only the curated common names; everything else
(interface TypedDicts, settings types, the remaining enums) is available here.
"""
from scrypted_sdk.scrypted_python.scrypted_sdk.types import * # noqa: F401,F403
'''
_ALTERNATION = "|".join(sorted(MODULES, key=len, reverse=True))
_FROM_RE = re.compile(rf"^(\s*)from ({_ALTERNATION})((?:\.[\w.]+)?) import ")
_IMPORT_RE = re.compile(rf"^(\s*)import ({_ALTERNATION})((?:\.[\w.]+)?)(\s+as\s+\w+)?\s*$")
@@ -112,6 +145,7 @@ def generate(source_dir: Path, package_dir: Path) -> None:
shutil.rmtree(package_dir)
package_dir.mkdir(parents=True)
(package_dir / "__init__.py").write_text(INIT_PY)
(package_dir / "types.py").write_text(TYPES_PY)
for flat, target in MODULES.items():
src = source_dir / f"{flat}.py"

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "scrypted-sdk"
version = "0.1.0"
version = "0.1.1"
description = "Python SDK for Scrypted: connect to a Scrypted server and use the same SDK objects plugins see"
readme = "README.md"
license = "ISC"

View File

@@ -119,6 +119,38 @@ class EioRpcTransport(rpc_reader.RpcTransport):
self._http_session = None
async def _login(
base_url: str,
username: str,
password: str,
timeout: float,
login_session: aiohttp.ClientSession | None,
) -> dict:
"""POST /login and return the parsed response.
Uses a temporary no-verify session when none is provided (and closes it);
a caller-provided session is left open. Raises ScryptedConnectionError on
any request failure.
"""
owns_session = login_session is None
session = login_session or aiohttp.ClientSession(
connector=aiohttp.TCPConnector(ssl=False)
)
try:
async with session.post(
f"{base_url}/login",
json={"username": username, "password": password},
raise_for_status=True,
timeout=aiohttp.ClientTimeout(total=timeout),
) as response:
return await response.json()
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
raise ScryptedConnectionError(f"Login to {base_url} failed: {err}") from err
finally:
if owns_session:
await session.close()
async def connect_scrypted_client(
loop: asyncio.AbstractEventLoop,
base_url: str,
@@ -142,29 +174,21 @@ async def connect_scrypted_client(
for initial system state. Raises :class:`ScryptedConnectionError` on any
failure.
"""
owns_login_session = login_session is None
session = login_session or aiohttp.ClientSession(
connector=aiohttp.TCPConnector(ssl=False)
)
try:
async with session.post(
f"{base_url}/login",
json={"username": username, "password": password},
raise_for_status=True,
timeout=aiohttp.ClientTimeout(total=timeout),
) as response:
login_response = await response.json()
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
raise ScryptedConnectionError(f"Login to {base_url} failed: {err}") from err
finally:
if owns_login_session:
await session.close()
if "authorization" not in login_response:
raise ScryptedConnectionError(
f"Login to {base_url} did not return an authorization header "
f"(response keys: {sorted(login_response)})"
login_response = await _login(
base_url, username, password, timeout, login_session
)
if "authorization" not in login_response:
raise ScryptedConnectionError(
f"Login to {base_url} did not return an authorization header "
f"(response keys: {sorted(login_response)})"
)
except ScryptedConnectionError:
# A caller-provided transport already owns a session and a running
# send loop; failing before the engine.io phase must not leak them.
if transport is not None:
await transport.close()
raise
if transport is None:
transport = EioRpcTransport(loop)