Skip to content

feat(ws): add Client.start_async() for use inside a running event loop#139

Open
mazhe-nerd wants to merge 1 commit into
v2_mainfrom
feature/ws-client-async-start-method
Open

feat(ws): add Client.start_async() for use inside a running event loop#139
mazhe-nerd wants to merge 1 commit into
v2_mainfrom
feature/ws-client-async-start-method

Conversation

@mazhe-nerd

Copy link
Copy Markdown
Collaborator

Summary

lark_oapi.ws.Client captured a module-level asyncio event loop at import time and drove start() via loop.run_until_complete(...). This fails when the client is used from inside an already-running event loop (FastAPI, aiohttp, or any async app) with RuntimeError: This event loop is already running, and it also breaks when the module is first imported inside a running loop.

This PR adds a native awaitable entry point and removes the import-time global loop.

Closes #96. Closes #133.

What's new

  • async def start_async(self) — await it from within your running event loop. Connection, reconnect, and event dispatch behave exactly like the synchronous start(), but run on the caller's loop. It blocks for the lifetime of the connection, so run it as a task if startup must continue:

    # FastAPI startup — run the bot in the background
    @app.on_event("startup")
    async def _startup():
        asyncio.create_task(cli.start_async())
    
    # or as the main coroutine
    asyncio.run(cli.start_async())
  • The module-level loop = asyncio.get_event_loop() that ran at import time is removed. Importing the WebSocket client no longer creates or mutates any global event loop; internal task scheduling now targets the running loop via asyncio.get_running_loop().

  • A runnable async sample is added at samples/ws/async_sample.py.

Compatibility

  • Backward compatible. Synchronous Client.start() is unchanged for normal callers (plain scripts / worker threads with no running loop): it creates its own loop and blocks as before.
  • Calling the synchronous start() from within a running loop now raises a clear, actionable RuntimeError pointing at start_async(), instead of the cryptic native This event loop is already running.
  • Removing the import-time get_event_loop() also drops the DeprecationWarning it emits on Python 3.10+.
  • No dependency or constructor-signature changes.

Tests

  • New unit tests covering: no import-time global loop, start_async() running on the caller's loop, the sync-in-running-loop guard, and error-propagation parity (ClientException vs. reconnect).
  • Existing websockets-compat tests updated to the new scheduling.
  • Verified on Python 3.9 and 3.12.

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

Labels

None yet

Projects

None yet

1 participant