Python SDK 3.1.2: lazy imports, attribution header, SSE forward-compat, answer safesearch - #49
Conversation
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
99ef0c9 to
ee36544
Compare
|
Droid finished @tyler5673's task —— View job Review summary: Good release hardening with strong test coverage, but there are a few consumer-facing contract mismatches to clean up (attribution arg validation vs builder behavior, and several docs claiming |
| # close. Today those clients hold no socket until their first request, | ||
| # so nothing actually leaks -- this keeps it that way if client | ||
| # construction ever starts acquiring a real resource. | ||
| if app_version is not None and app_name is None: |
There was a problem hiding this comment.
[P1] Treat empty app_name as unset for app_version validation
You.__init__ only rejects app_version when app_name is None, but build_client_info_header drops the client= segment when app_name is falsy, so app_name="" plus app_version="1" passes validation and then silently omits client= (and ignores the version). Consider normalizing empty strings to None (or using if app_version is not None and not app_name:) before validation so the constructor invariant matches the header builder.
| ] = None, | ||
| country: Optional[Union[str, models.Country]] = None, | ||
| language: Optional[Union[str, models.Language]] = None, | ||
| safesearch: Optional[str] = None, |
There was a problem hiding this comment.
[P2] Expose models.SafeSearch in answer() type hints
answer()/answer_async() currently declare safesearch: Optional[str], while the request model/docs use models.SafeSearch. Passing SafeSearch.STRICT works (it’s a str enum), but the signature doesn’t advertise the enum surface to typed callers. Consider changing the parameter type to Optional[Union[str, models.SafeSearch]] in both methods for consistency with the rest of the SDK’s “accept enum or plain string” pattern (and keep _lower(...) normalization).
| @@ -69,6 +69,7 @@ A synthesized answer with citations, grounded in live web results. | |||
| res = you.answer( | |||
There was a problem hiding this comment.
[P1] Make the Answer example runnable
The “Answer” snippet calls you.answer(...) but doesn’t show imports or constructing You (it assumes prior context); AGENTS.md treats docs/**/*.md example blocks as copy-paste runnable and requires showing with You(api_key_auth=os.getenv("YDC_API_KEY"), timeout_ms=60_000) as you: for network calls. Since this PR edits the snippet, please expand it to be self-contained (or restructure to make the dependency explicit).
| them as raw dicts. Prefer it over `you.stream_research_task()`, which validates | ||
| strictly and will raise on an unrecognized event. Pass `from_id` to resume a | ||
| stream after a disconnect. | ||
| `stream_research()` yields events as raw dicts and tolerates data frames that |
There was a problem hiding this comment.
[P1] Fix stream_research return-shape wording
README says stream_research() “yields events as raw dicts”, but the example uses attribute access (evt.event, evt.data) and stream_research actually yields RawStreamEvent objects; this mismatch encourages the wrong access pattern. Reword to reflect the real API shape (objects with .id/.event/.data) while keeping the note about tolerating non-JSON data frames.
| > `response.output_item.added`) that are not in this enum, which causes | ||
| > `ResponseValidationError` on the first intermediate event. The | ||
| > `stream_research()` helper uses a tolerant decoder that surfaces unknown | ||
| > event names as raw dicts instead of crashing. For real research tasks, prefer |
There was a problem hiding this comment.
[P1] Migration note misstates stream_research output type
In the “Note on streaming” block, stream_research() is described as surfacing unknown event names “as raw dicts”, but it yields RawStreamEvent objects and unknown names are just event: str. Updating the wording keeps the migration guide aligned with the actual helper API, especially now that unknown event names are also tolerated by stream_research_task().
| ``` | ||
|
|
||
| `app_name`, `app_version`, `app_title` and `app_url` are all optional. When | ||
| omitted, those segments are dropped entirely. Values must be printable ASCII (excluding `;`); invalid |
There was a problem hiding this comment.
[P1] Document '/' restriction for app_name/app_version
The attribution example says values must be printable ASCII excluding ;, but app_name and app_version also forbid / (they are joined as client=<name>/<version> and invalid values raise ValueError). Add the / restriction (and optionally the app_version requires app_name rule) so readers don’t hit unexpected constructor errors.
| documented enum, so unknown workflow events pass through instead of | ||
| failing pydantic validation. | ||
| documented enum, and unlike the pydantic path also accepts a ``data`` | ||
| payload that is not a JSON object at all. Since 3.1.2 the pydantic model |
There was a problem hiding this comment.
[P3] Keep research_helpers module docstring consistent
The _decode_raw_event docstring now correctly says the tolerant path’s advantage is shape tolerance (not unknown event-name tolerance), but the module-level description of stream_research at the top of research_helpers.py still claims it surfaces unknown event names “as raw dicts”. Updating that top-level bullet to match the new behavior (RawStreamEvent objects, shape tolerance) avoids contradictory guidance in the same module.
ee36544 to
2e64cbe
Compare
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
2e64cbe to
15cea20
Compare
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
15cea20 to
b0be494
Compare
|
Droid finished @tyler5673's task —— View job Overall the release changes look consistent and well-tested; the one high-signal follow-up is to fix the |
| :param app_version: Optional version paired with ``app_name`` as | ||
| ``client=<name>/<version>``. Ignored when ``app_name`` is unset. | ||
| Must be printable ASCII (excluding ``;`` and ``/``). |
There was a problem hiding this comment.
[P1] Fix app_version docstring contract
You.__init__ raises ValueError when app_version is set without a truthy app_name, so the current docstring line “Ignored when app_name is unset” is misleading for callers (it suggests silent drop rather than a hard error). Update the docstring to state that app_version requires app_name.
| :param app_version: Optional version paired with ``app_name`` as | |
| ``client=<name>/<version>``. Ignored when ``app_name`` is unset. | |
| Must be printable ASCII (excluding ``;`` and ``/``). | |
| :param app_version: Optional version paired with ``app_name`` as | |
| ``client=<name>/<version>``. Requires ``app_name``; passing it without | |
| ``app_name`` raises ``ValueError``. Must be printable ASCII (excluding ``;`` and ``/``). |
b0be494 to
7edef95
Compare
|
Droid finished @tyler5673's task —— View job Review summary: Looks solid overall with good test coverage; the only issue spotted is a small mismatch in the README streaming example’s terminal-event break condition. |
| them as raw dicts. Prefer it over `you.stream_research_task()`, which validates | ||
| strictly and will raise on an unrecognized event. Pass `from_id` to resume a | ||
| stream after a disconnect. | ||
| `stream_research()` yields `RawStreamEvent` objects, whose `.event` is the raw |
There was a problem hiding this comment.
[P1] Add complete to README stream terminal check
In the stream_research() example above, the loop breaks on "completed" but not "complete". The SDK treats both as terminal stream events (research_helpers._TERMINAL_STREAM_EVENTS_OK includes "complete"), so the snippet should include it too.
if evt.event in ("response.done", "complete", "completed", "error", "failed", "cancelled"):
break7edef95 to
fc021c3
Compare
|
Droid finished @tyler5673's task —— View job Validated the candidate comments, both are accurate and actionable. Main follow-ups are fixing case-insensitive |
| # site as ``User-Agent`` — every endpoint funnels through | ||
| # ``_build_request_with_client``, so a single construction | ||
| # point prevents per-endpoint drift. | ||
| headers["X-Client-Info"] = build_client_info_header( |
There was a problem hiding this comment.
[P1] Make http_headers override case-insensitive for X-Client-Info
BaseSDK._build_request_with_client now sets headers["X-Client-Info"] unconditionally, but later merges caller http_headers by exact key, so http_headers={"x-client-info": "caller-wins"} can produce two X-Client-Info header lines (httpx preserves both and may coalesce them as "a, b"), breaking the intended “caller override wins” contract and the header grammar; fix by deleting any existing header whose name matches case-insensitively before setting the caller-provided value.
| | `exclude_domains` | Optional[List[*str*]] | :heavy_minus_sign: | Domains to exclude. Cannot combine with `include_domains`. Can combine with `boost_domains`. Max 500. | | | ||
| | `boost_domains` | Optional[List[*str*]] | :heavy_minus_sign: | Domains to prefer in ranking. Cannot combine with `include_domains`. Can combine with `exclude_domains`. Max 500. | | | ||
| | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | | ||
| | `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | |
There was a problem hiding this comment.
[P3] Document answer() per-call timeout_ms and http_headers
The answer Parameters table includes SDK-level knobs like retries and server_url, but omits per-call timeout_ms and http_headers even though they are accepted by You.answer()/You.answer_async() in src/youdotcom/sdk.py, which makes the docs misleading for users trying to set a longer timeout or override headers on a single request; add rows for those parameters (matching the surrounding table format) so the documented callable surface matches the actual signature.
fc021c3 to
4d872b4
Compare
|
Droid finished @tyler5673's task —— View job Changes look consistent and well-covered by tests. The main follow-up is making the new subprocess-based root-import tests robust under src-layout (set PYTHONPATH and optionally cwd) so they do not assume the package is installed. |
| pass ``-S``: that flag disables the venv's ``site.py`` shim and | ||
| would render the SDK uninstalled for the subprocess. | ||
| """ | ||
| result = subprocess.run( |
There was a problem hiding this comment.
[P1] Make subprocess imports match pytest src-layout
_run_in_subprocess() spawns a fresh interpreter that does not inherit pytest's pythonpath = ["src"] injection, so import youdotcom can fail in environments where the package is not installed (a supported local-run mode for this repo's src-layout). Pass PYTHONPATH=<repo>/src (and ideally cwd=<repo>) to subprocess.run(...) so the subprocess sees the same import surface as the parent test process.
4d872b4 to
d5f9439
Compare
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
d5f9439 to
16e69f0
Compare
|
Droid finished @tyler5673's task —— View job Overall the 3.1.2 release PR looks solid and well-tested; the remaining follow-ups are a broken MIGRATION.md link, an Answer safesearch docs contract mismatch between surfaces, and a small hardening gap in |
| httpx_version = str(getattr(httpx, "__version__", "unknown")) | ||
| if not httpx_version.isascii() or any(c in httpx_version for c in ";/"): | ||
| httpx_version = "unknown" |
There was a problem hiding this comment.
[P2] Sanitize httpx.version for ASCII control characters
build_client_info_header() tries to degrade a weird/patched httpx.__version__ to unknown, but the current isascii() + ;// check still allows ASCII control characters like \r/\n, which can cause header encoding failures and undermines the comment’s intent to avoid breaking requests in forked environments. Reuse validate_attribution_arg’s printable-ASCII rules (and still forbid /) before interpolating the version into the ua= segment.
| httpx_version = str(getattr(httpx, "__version__", "unknown")) | |
| if not httpx_version.isascii() or any(c in httpx_version for c in ";/"): | |
| httpx_version = "unknown" | |
| httpx_version = str(getattr(httpx, "__version__", "unknown")) | |
| try: | |
| validate_attribution_arg("httpx.__version__", httpx_version, forbidden="/") | |
| except ValueError: | |
| httpx_version = "unknown" |
| | `freshness` | [Optional[models.FreshnessValue]](../../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results. One of `day`, `week`, `month`, `year`, or `YYYY-MM-DDtoYYYY-MM-DD`. | | | ||
| | `country` | [Optional[models.Country]](../../models/country.md) | :heavy_minus_sign: | A supported country code that determines the geographical focus of the web results. | | | ||
| | `language` | [Optional[models.Language]](../../models/language.md) | :heavy_minus_sign: | A supported BCP 47 language tag that determines the language of the web results. | | | ||
| | `safesearch` | [Optional[models.SafeSearch]](../../models/safesearch.md) | :heavy_minus_sign: | Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. | | |
There was a problem hiding this comment.
[P1] Align answer(safesearch=...) docs across surfaces
This safesearch row’s Description is currently the shorter, generic blurb, while docs/sdks/answer/README.md documents the actual contract (allowed values off/moderate (default)/strict, case-insensitive). The repo’s doc convention requires the same parameter description to match across the You SDK table, the endpoint-specific SDK table, and the request-body model page, otherwise users get conflicting guidance about valid values and defaults.
|
|
||
| | Change | Who is affected | What to do | | ||
| |--------|-----------------|------------| | ||
| | Import machinery no longer re-exported from the package root | Anyone importing a stdlib/typing name or internal helper *from* `youdotcom` | Import it from its real home. See [Root namespace narrowing](#root-namespace-narrowing-1) | |
There was a problem hiding this comment.
[P1] Fix broken intra-doc anchor for Root namespace narrowing
In the 3.1.1 → 3.1.2 “Action required” table, the link target is #root-namespace-narrowing-1, but this file only has a single “Root namespace narrowing” heading, so GitHub generates #root-namespace-narrowing (no -1) and the link is broken.
| | Import machinery no longer re-exported from the package root | Anyone importing a stdlib/typing name or internal helper *from* `youdotcom` | Import it from its real home. See [Root namespace narrowing](#root-namespace-narrowing-1) | | |
| | Import machinery no longer re-exported from the package root | Anyone importing a stdlib/typing name or internal helper *from* `youdotcom` | Import it from its real home. See [Root namespace narrowing](#root-namespace-narrowing) | |
…t, answer safesearch Fixed - Models are usable inside a Temporal Workflow. The package root resolves its public names lazily via PEP 562 `__getattr__`, so `import youdotcom` no longer pulls `httpx` / `urllib.request` into `sys.modules` and a Workflow module can import SDK models without a `workflow.unsafe.imports_passed_through()` work-around. - `ResearchTaskStreamEvent.event` accepts future SSE event names. `Event` uses `OpenEnumMeta` and the field is declared `EventName` (`Union[Event, str]`), which is what it holds at runtime: known names resolve to `Event` members, unknown names stay plain `str` instead of raising `ResponseValidationError`. Added - `X-Client-Info` attribution header on every outbound request, with optional keyword-only `You(app_name=..., app_version=..., app_title=..., app_url=...)` so a caller can identify itself. Values must be printable ASCII excluding `;` (and `/` for `app_name` / `app_version`, which are joined as `<name>/<version>`), validated before any transport is constructed. The leading `sdk` token names the channel, matching the `mcp` token the You.com MCP server emits in the same position. The calling language stays recoverable from `ua=`, which no wrapping integration can override, and the SDK's own name and version from the `User-Agent`. `client=` identifies the caller, not the SDK, and is dropped entirely when `app_name` is unset. That keeps the segment meaningful: an undeclared caller emits `sdk; ua=python/...`, while an integration emits its own `client=<name>/<version>`, so direct SDK use and wrapping integrations are distinguishable without relying on convention. The SDK never emits `X-MCP-Attribution`; that header belongs to the MCP server. - `safesearch` on `You.answer()` / `answer_async()`, matching `search()`. Root namespace narrowing Dropping the eager `from .sdk import *` also stops it leaking its own imports onto `youdotcom`. 24 of the 48 public names the root carried on 3.1.1 were import machinery, not API: `httpx`, `asyncio`, `warnings`, `weakref`, the typing and dataclass helpers, internal helpers (`eventstreaming`, `get_security_from_env`, `unmarshal_json_response`, `remove_suffix`), and the private submodule aliases (still importable directly). Every documented name still resolves from the root and still binds under `from youdotcom import *`. The version dunders stay out of `__all__` so a star import cannot overwrite a consumer package's own `__version__`. See MIGRATION.md "3.1.1 -> 3.1.2". Notes - `sdk.py` resolves the `crawl_timeout` default inside its branch so the common path never forces `models` to load; astroid cannot infer through the lazy root, so the subscript carries a targeted pylint disable. - Verified: mypy clean (82 files), `pylint --enable=E` gate exit 0, pyright clean on the root, 302 tests passing, no OpenAPI drift, and the attribution header accepted by the live API. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
16e69f0 to
dfd4ca6
Compare
|
Droid finished @tyler5673's task —— View job Review summary: No additional high-confidence, actionable issues found beyond existing PR comments. |
- Case-insensitive X-Client-Info override: delete existing headers that case-insensitively match caller-provided keys before merging, so httpx doesn't coalesce two entries as "a, b" - httpx.__version__ sanitization: replace isascii() with validate_attribution_arg to reject ASCII control characters - answer()/answer_async() safesearch type hint: Optional[str] -> Optional[Union[str, models.SafeSearch]] for enum discoverability - MIGRATION.md: fix broken anchor #root-namespace-narrowing-1 -> #root-namespace-narrowing - README.md: add note that API snippets assume the Quickstart client - Tests: case-insensitive header override + control char degradation Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
Droid finished @tyler5673's task —— View job Review summary: No additional high-confidence, actionable issues found beyond existing PR comments. (Inline comments posted: 0) |
Python SDK 3.1.2 release
Every documented surface is backward-compatible. The one exception is the root
namespace narrowing described below, which affects only names that were never
part of the public API.
What's in this release
Fixed
Models are usable inside a Temporal Workflow.
youdotcom/__init__.pyno longer eagerly pulls transport-layer modules(including
httpxandurllib.request) intosys.modules. Any Workflowfile that does
from youdotcom.models import SearchResponse(without aworkflow.unsafe.imports_passed_through()work-around) now prepares cleanlyunder the default
SandboxedWorkflowRunner. PEP 562 module__getattr__mirrors the public-import surface without dragging transport.
ResearchTaskStreamEvent.eventaccepts future SSE event names.Eventnow usesOpenEnumMeta, and the field is declaredEventName(
Union[Event, str]) because that is what it holds at runtime. Known namesresolve to
Eventmembers; unknown names stay plainstr, so a server-sideevent addition no longer raises
ResponseValidationError. Callers branchingon raw strings (
evt.event == "completed") keep working unchanged.Typed callers may see a new type error:
evt.event.valueno longertype-checks. That is the bug surfacing, not a new restriction — the same code
raises
AttributeErrorat runtime the first time the server emits anunenumerated name. Guard with
isinstance(evt.event, Event).Added
Attribution header
X-Client-Infoon every outbound request.New optional
You(app_name=..., app_version=..., app_title=..., app_url=...)keyword arguments let a caller identify itself, so You.com can tell SDK
traffic apart from other clients and see which applications and integrations
are in use. Wire format:
Values must be printable ASCII excluding
;, andapp_name/app_versionadditionally exclude
/, since they are joined as<name>/<version>.Invalid values raise
ValueErrorat construction time, before any transportis created. All four arguments are optional and keyword-only, so later
additions are not a breaking change.
How the segments are meant to be read:
sdkmcpin the same position, so a single lowercase token keeps the two consistent.ua=python/3.11.15 httpx/0.28.1. Built by the SDK, so it is a reliable indicator of which language SDK made the call.client=app_nameis unset, so an undeclared caller sendssdk; ua=python/…. The SDK's own name and version travel in theUser-Agent.title=/url=A wrapping integration can therefore be distinguished from direct SDK use:
Integrations that override
sdk_configuration.user_agentshould appendrather than replace it, so the SDK's own version stays visible.
The SDK never emits
X-MCP-Attribution; that header is set by the You.comMCP server, which is the only layer that can populate its flags accurately.
A negative test pins this.
safesearchparameter onYou.answer().New optional
safesearchkeyword argument onanswer()/answer_async()accepting
off,moderate(default), orstrict. Case-insensitive. Bringsthe Answer API in line with
search(), which already exposed it.Root namespace narrowing
Replacing the eager
from .sdk import */from .sdkconfiguration import *also stops those statements leaking their own imports onto
youdotcom. On3.1.1 the package root carried 48 public names; 24 were import machinery
rather than API and are gone:
httpx,asyncio,warnings,weakref, thetyping and dataclass helpers (
Any,Callable,Dict,Iterable,List,Mapping,Optional,Tuple,Union,cast,dataclass,field),internal helpers (
eventstreaming,get_security_from_env,unmarshal_json_response,remove_suffix), and the private submodule aliases(
sdk,basesdk,httpclient,sdkconfiguration— still importabledirectly as
import youdotcom.sdk).None were documented, exported deliberately, or referenced by any example.
Every documented name still resolves from the root and still binds under
from youdotcom import *.BackoffStrategyis newly resolvable from the rootalongside
RetryConfig.The version dunders (
__version__,__title__,__user_agent__,__openapi_doc_version__) are deliberately not in__all__, so a starimport cannot overwrite a consumer package's own
__version__. They remainreachable as attributes.
MIGRATION.mdgains a3.1.1 → 3.1.2section covering this and theevt.event.valuetyping change.Verification
pytest(excluding live and mockserver-dependent tests): 302 passed.header is accepted by the real gateway and not only by a mock.
mypy src/youdotcom/: clean, 82 source files.pylint src/youdotcom/ --disable=all --enable=E --rcfile=/dev/null(the CIgate): exit 0. No positional-argument creep versus
main.pyright src/youdotcom/__init__.py: 0 errors, 0 warnings.scripts/check_drift.py: no drift across 7 specs, which independentlyconfirms
safesearchon/v1/answermatches the live contract.-W error::UserWarning(zero serialization warnings).Tests added
tests/test_root_init.py— transport-free import invariant (asserted in asubprocess),
import *binds every__all__name and the sub-packages areusable, and
import *does not clobber a consumer's__version__.tests/test_attribution.py— wire-format grammar, validation (segmentforgery, header injection, non-ASCII, control characters,
/inclient=halves), construction-time fail-fast, callerhttp_headersoverride precedence, direct-use vs integration separation, keyword-only
signature,
X-MCP-Attributionnever sent (sync and async), and versionresolution through
importlib.metadataincluding the uninstalled-checkoutfallback.
tests/test_researchtaskstreamevent.py— known and unknown eventround-trips, the declared-type contract, and a regression test driving the
real
stream_research_taskSSE decode path with unknown event names(verified to fail with
ResponseValidationErroragainst the pre-fix code).tests/test_live.py—X-Client-Infosent and accepted on real requests,X-MCP-Attributionabsent,answer(safesearch=STRICT).Release artifacts
pyproject.toml+src/youdotcom/_version.pybumped to3.1.2uv.lockauto-bumpedCHANGELOG.md## [3.1.2] - 2026-08-20entryMIGRATION.md3.1.1 → 3.1.2sectionREADME.md### Attributionsubsection,safesearchin the answer exampleUSAGE.md[attribution]example blockdocs/models/answerrequestbody.mdsafesearchrowdocs/models/eventname.md(new),docs/models/event.mdopen-enum note,docs/models/researchtaskstreamevent.mdretyped tomodels.EventNamedocs/sdks/you/README.mdanswerparameter table rebuilt in thehouse-standard format with
Response/Errorssections🤖 Generated with Claude Code