Skip to content

feat: Add AsyncSSEClient with aiohttp-based async/await support#58

Open
keelerm84 wants to merge 3 commits intomainfrom
mk/sdk-1400/async
Open

feat: Add AsyncSSEClient with aiohttp-based async/await support#58
keelerm84 wants to merge 3 commits intomainfrom
mk/sdk-1400/async

Conversation

@keelerm84
Copy link
Member

@keelerm84 keelerm84 commented Mar 11, 2026

Adds AsyncSSEClient as a purely additive new public API alongside the
existing SSEClient. Async users install with the [async] extra to get
aiohttp; sync users have no new dependencies. All existing tests pass
unchanged.


Note

Medium Risk
Adds a new async client and HTTP stack (aiohttp) with new retry/streaming behavior and contract-test infrastructure; although largely additive and behind an optional extra, it introduces new concurrency/resource-lifecycle paths that could affect reliability if misused.

Overview
Adds a new public async/await SSE API (AsyncSSEClient) built around aiohttp, including async stream parsing (_AsyncBufferedLineReader/_AsyncSSEReader), an async connection abstraction (AsyncConnectStrategy), and an async HTTP connector with header/query-param support and response validation.

Updates packaging and docs to keep async dependencies optional (launchdarkly-eventsource[async]) and lazily exposes AsyncSSEClient via ld_eventsource.__getattr__ to avoid importing aiohttp for sync-only users.

Extends test coverage and automation by adding pytest-asyncio-based unit tests, an async contract-test service (contract-tests/async_service.py), Makefile targets, and CI steps to run the async contract tests; also mocks aiohttp during doc builds.

Written by Cursor Bugbot for commit 069bffe. This will update automatically on new commits. Configure here.

Adds AsyncSSEClient as a purely additive new public API alongside the
existing SSEClient. Async users install with the [async] extra to get
aiohttp; sync users have no new dependencies. All existing tests pass
unchanged.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@keelerm84 keelerm84 marked this pull request as ready for review March 11, 2026 19:52
@keelerm84 keelerm84 requested a review from a team as a code owner March 11, 2026 19:52
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

keelerm84 and others added 2 commits March 11, 2026 16:44
Removes make_stream, retry_for_status, and no_delay from async_helpers.py
as they were either unused or duplicates of the same functions in helpers.py.
Updates async test files to import these from helpers.py consistently.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

request_options = {}
if self.options.get("readTimeoutMs") is not None:
request_options["timeout"] = aiohttp.ClientTimeout(
Copy link
Member

Choose a reason for hiding this comment

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

I think, even without a read timeout, we need to customize the ClientTimeout.
image

The timeout uses a non-default ClientTimeout.

But if we do set it to anything, then the total will get set to None. https://docs.aiohttp.org/en/stable/client_reference.html#aiohttp.ClientTimeout.total

But if we set a ClientTimeout, we would get the default total=None.

error = e
self.__connection_result = None
finally:
self.__last_event_id = reader.last_event_id
Copy link
Member

Choose a reason for hiding this comment

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

Should we close the result in finally out of an abundance of caution? By the docs you normally don't need to close it if it completes normally, and it would be closed on a network error, but the exception here here could also potentially be a decoding error, in which maybe we wouldn't close it?


async def close(self):
# Only close the session if we created it ourselves
if self.__external_session is None and self.__session is not None:
Copy link
Member

Choose a reason for hiding this comment

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

I think the non-async version does this wrong?

self.__should_close_pool = params.pool is not None

self.__should_close_pool = params.pool is not None

Not a problem with this PR, but maybe worth checking.

Copy link
Member

@kinyoklion kinyoklion left a comment

Choose a reason for hiding this comment

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

Requests in comments.

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.

2 participants