Repository navigation
Conversation
…lient A script, a batch job or a Django view can now call every client method without await and without writing its own asyncio.run wrapper. The facade wraps and delegates to one private PipelexAPIClient running on a daemon event-loop thread, so there is still exactly one HTTP implementation; one instance is shareable across threads, and a call from inside a running event loop raises SyncClientInEventLoopError before anything is sent. A parity test pins every public coroutine's sync twin to an identical signature. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…heir contract under concurrency
The first review round on the sync facade (L-260907-91b052) reproduced these defects, and each now has a regression test that fails on the previous code.
- A call made on another thread while close() ran started a second loop on the HTTP client the first loop was still closing, and failed with "Cannot send a request, as the client has been closed". close() now holds the facade's lock until the old loop is gone. The lock is reentrant, so an abandoned iterator that a garbage collection finalizes under it no longer deadlocks its own thread.
- A KeyboardInterrupt or SystemExit raised on the loop, such as an on_poll callback calling sys.exit(), stopped the loop and left both the caller and close() blocked for good. Both are now carried back to the caller, and the loop survives.
- Ctrl+C during a page fetch surfaced as RuntimeError("aclose(): asynchronous generator is already running") instead of the interrupt. The async generator is now finalized only when its iterator is abandoned between two items.
- A paged iterator left open across close() ended silently, as if the listing were complete. It now raises concurrent.futures.CancelledError at its next item.
- A partly read iterator left in a module global kept the interpreter from exiting, because it waited on a daemon loop thread that had already stopped.
- The in-loop refusal named the wrapped coroutine (MthdsAPIClient.models, _next_item) instead of the facade method that was called.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…the result The parity test compared signatures only, so a twin that dropped or swapped an argument passed the whole suite; mutating iterate_methods to forward limit=None proved it. Each twin is now called with a distinct sentinel per parameter and must hand every one to its coroutine under its own name, then return what the coroutine returned. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
Review round 1: profile 4 (deep), bar Reviewers that produced a review: cubic, Codex ( Fixed in c742052, each with a regression test that fails on the previous code:
Fixed in d0c0fbc: the parity test also pins that every twin forwards each argument and hands back the result. A twin dropping an argument used to pass the whole suite. Deferred:
|
|
Closing unmerged. The two review rounds showed that sharing one facade across threads through a background event loop does not converge, so the facade will be rebuilt on |
Adds
SyncPipelexAPIClient(pipelex_sdk/sync_client.py), a synchronous facade overPipelexAPIClient, so a script, a batch job or a Django view calls every method withoutawaitor anasyncio.runwrapper of its own. It wraps and delegates to one private async client on a daemon event-loop thread rather than adding a second transport, so the connection pool stays warm and one instance can be shared across threads; a call from inside a running event loop raises the newSyncClientInEventLoopErrorbefore anything is sent. A parity test pins each sync method's signature to its async twin in both directions, and the docs, README and changelog describe the facade.Closes L-260907-91b052
🤖 Generated with Claude Code