Files
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

166 lines
5.8 KiB
Python

"""Concrete transport kernel for NotebookLM session operations."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from typing import Any
import httpx
from ._request_types import PostBody
from ._streaming_post import stream_post_with_size_cap
from .auth import AuthTokens, build_cookie_jar
from .types import ConnectionLimits
class Kernel:
"""Own the live HTTP transport and cookie jar.
Client lifecycle code decides when to open and close. The kernel owns the
concrete ``httpx.AsyncClient`` instance, its cookie jar, raw POST execution,
and shielded teardown target.
"""
def __init__(
self,
*,
async_client_factory: Callable[..., httpx.AsyncClient] = httpx.AsyncClient,
) -> None:
self._async_client_factory = async_client_factory
self._http_client: httpx.AsyncClient | None = None
self._timeout: float | None = None
self._connect_timeout: float | None = None
@property
def http_client(self) -> httpx.AsyncClient | None:
"""Return the live HTTP client, or ``None`` when closed.
The property is read-only by design. Production code mutates the
underlying client only through :meth:`open` (which constructs the
live client via the injected ``async_client_factory``) and
:meth:`aclose` (which nulls it on teardown). Tests that need to
substitute the live transport at construction time should inject
an ``async_client_factory`` into the client composition helper
(the factory is forwarded into this kernel's ``__init__``); tests
that need to swap the live client AFTER ``open()`` should use the
dedicated test helper at
``tests/_fixtures/kernel_test_helpers.py``.
"""
return self._http_client
@property
def cookies(self) -> httpx.Cookies:
"""Return the live HTTP client's cookie jar.
Raises ``RuntimeError`` if called before :meth:`open`.
"""
return self.get_http_client().cookies
def get_http_client(self) -> httpx.AsyncClient:
"""Return the live HTTP client or raise the legacy not-open error."""
if self._http_client is None:
raise RuntimeError("Client not initialized. Use 'async with' context.")
return self._http_client
async def open(
self,
*,
auth: AuthTokens,
timeout: float,
connect_timeout: float,
limits: ConnectionLimits,
capture_cookie_snapshot: Callable[[httpx.Cookies], object],
) -> None:
"""Build the HTTP client and capture its normalized cookie baseline."""
# ClientLifecycle owns the primary idempotency guard. Keep this
# secondary guard so direct Kernel callers also preserve the live client.
if self._http_client is not None:
return
http_timeout = httpx.Timeout(
connect=connect_timeout,
read=timeout,
write=timeout,
pool=timeout,
)
self._timeout = timeout
self._connect_timeout = connect_timeout
cookies = (
auth.cookie_jar
if auth.cookie_jar is not None
else build_cookie_jar(
cookies=auth.cookies,
storage_path=auth.storage_path,
)
)
self._http_client = self._async_client_factory(
headers={
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
cookies=cookies,
timeout=http_timeout,
follow_redirects=True,
limits=limits.to_httpx_limits(),
)
# If the snapshot raises, __aenter__ has effectively failed, so Python
# never calls __aexit__ and the freshly built client would leak its
# connection pool. Close it and reset state before re-raising so a
# partial open cannot orphan a live client.
try:
capture_cookie_snapshot(self._http_client.cookies)
except BaseException:
await self.aclose()
raise
async def post(
self,
url: str,
headers: Mapping[str, str] | None,
body: PostBody,
*,
read_timeout: float | None = None,
max_response_bytes: int | None = None,
) -> httpx.Response:
"""Issue a raw buffered POST through the live HTTP client."""
timeout_override: httpx.Timeout | None = None
if read_timeout is not None:
# Widen ONLY the read (inactivity) slot; keep connect/write/pool at
# the stored base values verbatim — including ``None``, which httpx
# treats as "no timeout", so an explicit infinite-timeout client
# keeps that intent on its other slots instead of inheriting the
# chat read window.
timeout_override = httpx.Timeout(
connect=self._connect_timeout,
read=read_timeout,
write=self._timeout,
pool=self._timeout,
)
headers_arg = dict(headers) if headers is not None else None
stream_kwargs: dict[str, Any] = {}
if max_response_bytes is not None:
stream_kwargs["max_bytes"] = max_response_bytes
return await stream_post_with_size_cap(
self.get_http_client(),
url,
body=body,
headers=headers_arg,
timeout=timeout_override,
**stream_kwargs,
)
async def aclose(self) -> None:
"""Close the live HTTP client and mark the kernel closed."""
client = self._http_client
if client is None:
return
try:
await client.aclose()
finally:
self._http_client = None
self._timeout = None
self._connect_timeout = None
__all__ = ["Kernel"]