mirror of
https://github.com/koush/scrypted.git
synced 2026-08-02 19:31:34 +01:00
python-sdk: 0.1.1 — top-level type exports and transport leak fix (#2109)
This commit is contained in:
2
.github/workflows/python-sdk.yml
vendored
2
.github/workflows/python-sdk.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
run: |
|
||||
uv venv /tmp/smoke
|
||||
uv pip install --python /tmp/smoke/bin/python dist/*.whl
|
||||
/tmp/smoke/bin/python -c "import scrypted_sdk; print(scrypted_sdk.connect_scrypted_client)"
|
||||
/tmp/smoke/bin/python -c "import scrypted_sdk; from scrypted_sdk.types import VideoClip, LockState; print(scrypted_sdk.connect_scrypted_client, scrypted_sdk.PluginRemote, scrypted_sdk.ScryptedInterface.MotionSensor, VideoClip, LockState)"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-sdk-dist
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user