How the Async Context Manager Pattern Works with NotebookLMClient
NotebookLMClient implements the async context manager pattern to automatically open HTTP connections when entering an async with block and guarantee cleanup via __aexit__, preventing resource leaks while handling authentication refreshes transparently.
The teng-lin/notebooklm-py library provides a Python interface to Notebook LM's API, with NotebookLMClient serving as the primary entry point. Understanding how the async context manager pattern manages the client's lifecycle is essential for writing robust asynchronous code that handles network resources efficiently.
Factory Initialization Without Network I/O
The entry point NotebookLMClient.from_storage() creates a client instance without immediately opening network connections. This classmethod reads stored authentication tokens from Playwright's storage state and returns a initialized client that remains dormant until the async context is entered.
client = await NotebookLMClient.from_storage() # reads stored auth tokens
According to the source code in src/notebooklm/client.py (lines 19-43), this factory builds an AuthTokens object from the storage file but defers HTTP client creation until __aenter__ is called. This lazy initialization ensures that network resources are allocated only when actually needed.
Entering the Context with aenter
When execution enters the async with block, Python invokes NotebookLMClient.__aenter__. This method delegates to the internal ClientCore.open() method to establish the HTTP session.
async def __aenter__(self) -> "NotebookLMClient":
logger.debug("Opening NotebookLM client")
await self._core.open()
return self
As implemented in src/notebooklm/client.py (lines 101-108), __aenter__ returns the client instance after initialization. The heavy lifting occurs in ClientCore.open() at src/notebooklm/_core.py (lines 24-45), which instantiates an httpx.AsyncClient with proper headers—including authentication cookies, CSRF tokens, and timeout configuration—making the client ready for RPC calls.
Guaranteed Cleanup via aexit
When the async with block exits—whether normally or via an exception—Python automatically calls NotebookLMClient.__aexit__. This ensures the underlying HTTP client closes properly, returning sockets to the operating system.
async def __aexit__(self, exc_type, exc_val, exc_tb):
logger.debug("Closing NotebookLM client")
await self._core.close()
The implementation in src/notebooklm/client.py (lines 109-115) forwards the close operation to ClientCore.close(), which asynchronously shuts down the httpx.AsyncClient and releases all references. This pattern eliminates connection leaks even when exceptions occur during API operations.
Benefits of the Async Context Manager Pattern
Using NotebookLMClient within an async context manager provides three critical advantages:
- Resource Safety: The HTTP session lifespan is strictly bound to the
async withblock, preventing orphaned connections and file descriptor exhaustion. - Automatic Token Refresh:
ClientCoreaccepts a refresh callback (NotebookLMClient.refresh_auth). When RPC calls encounter authentication errors, the core transparently refreshes tokens and retries requests while maintaining the active context. - Shared State: All sub-clients (
NotebooksAPI,SourcesAPI,ChatAPI) receive the sameClientCoreinstance during construction, sharing a single HTTP connection pool and refresh logic throughout the context lifetime.
Practical Implementation Examples
Basic Notebook Listing
This example demonstrates the complete lifecycle: connection establishment, API call, and automatic cleanup.
import asyncio
from notebooklm import NotebookLMClient
async def list_notebooks():
async with await NotebookLMClient.from_storage() as client:
notebooks = await client.notebooks.list()
for nb in notebooks:
print(nb["id"], nb["title"])
asyncio.run(list_notebooks())
The await NotebookLMClient.from_storage() call initializes the client; the async with block opens the connection, executes the RPC, and automatically closes resources upon exit.
Custom Authentication and Timeouts
For scenarios requiring manual authentication or custom timeout values:
from notebooklm import NotebookLMClient
from notebooklm.auth import AuthTokens
async def use_custom_auth():
auth = AuthTokens(cookies="...", csrf_token="...", session_id="...")
async with NotebookLMClient(auth, timeout=60.0) as client:
await client.sources.add_url("nb123", "https://example.com")
Handling Token Refresh Automatically
The context manager handles authentication expiration seamlessly during long-running operations:
async def demo_refresh():
async with await NotebookLMClient.from_storage() as client:
# If this call encounters an auth error, the core invokes
# client.refresh_auth() automatically, updates headers,
# and retries the RPC before returning.
result = await client.chat.ask("nb123", "What is AI?")
print(result)
Core Architecture and Source Files
The async context manager pattern spans several key files in the teng-lin/notebooklm-py repository:
| File | Role |
|---|---|
src/notebooklm/client.py |
Implements NotebookLMClient with __aenter__, __aexit__, from_storage() factory, and refresh_auth() callback |
src/notebooklm/_core.py |
Contains ClientCore class managing httpx.AsyncClient lifecycle via open() and close() methods |
src/notebooklm/auth.py |
Defines AuthTokens dataclass storing cookies, CSRF tokens, and session identifiers |
src/notebooklm/_chat.py (and other _*.py modules) |
Namespace sub-clients receiving the shared ClientCore instance for RPC operations |
Summary
- Lazy Initialization:
NotebookLMClient.from_storage()creates clients without opening network connections, deferring I/O until context entry. - Automatic Resource Management: The
__aenter__and__aexit__methods insrc/notebooklm/client.pyguarantee thathttpx.AsyncClientinstances open and close correctly. - Resilient Authentication: The context manager integrates token refresh logic, allowing automatic recovery from auth failures without manual intervention.
- Connection Sharing: All API namespaces share a single
ClientCoreinstance, ensuring efficient connection reuse throughout the async context.
Frequently Asked Questions
What happens if an exception occurs inside the async with block?
The __aexit__ method receives the exception details via exc_type, exc_val, and exc_tb parameters and still executes await self._core.close(), ensuring the HTTP client shuts down properly before the exception propagates upward.
Can I use NotebookLMClient without the async context manager?
While technically possible by manually calling await client._core.open() and await client._core.close(), this approach bypasses the safety guarantees of the async context manager pattern and requires you to handle cleanup in exception scenarios manually, which is not recommended.
How does automatic token refresh work within the context manager?
ClientCore stores a reference to NotebookLMClient.refresh_auth() as a callback. When any RPC call returns an authentication error, ClientCore automatically invokes this callback to refresh tokens, updates the HTTP headers, and retries the failed request—all while maintaining the active context and connection.
Is the HTTP client shared across different API endpoints?
Yes. Sub-clients like client.notebooks, client.sources, and client.chat all receive the same ClientCore instance during NotebookLMClient initialization, meaning they share a single httpx.AsyncClient and benefit from connection pooling and unified token refresh logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →