Skip to content

Python SDK 3.1.2: lazy imports, attribution header, SSE forward-compat, answer safesearch - #49

Merged
tyler5673 merged 2 commits into
mainfrom
release/3.1.2
Aug 21, 2026
Merged

Python SDK 3.1.2: lazy imports, attribution header, SSE forward-compat, answer safesearch#49
tyler5673 merged 2 commits into
mainfrom
release/3.1.2

Conversation

@tyler5673

@tyler5673 tyler5673 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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__.py no longer eagerly pulls transport-layer modules
    (including httpx and urllib.request) into sys.modules. Any Workflow
    file that does from youdotcom.models import SearchResponse (without a
    workflow.unsafe.imports_passed_through() work-around) now prepares cleanly
    under the default SandboxedWorkflowRunner. PEP 562 module __getattr__
    mirrors the public-import surface without dragging transport.

  • ResearchTaskStreamEvent.event accepts future SSE event names.
    Event now uses OpenEnumMeta, and the field is declared EventName
    (Union[Event, str]) because that is what it holds at runtime. Known names
    resolve to Event members; unknown names stay plain str, so a server-side
    event addition no longer raises ResponseValidationError. Callers branching
    on raw strings (evt.event == "completed") keep working unchanged.

    Typed callers may see a new type error: evt.event.value no longer
    type-checks. That is the bug surfacing, not a new restriction — the same code
    raises AttributeError at runtime the first time the server emits an
    unenumerated name. Guard with isinstance(evt.event, Event).

Added

  • Attribution header X-Client-Info on 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:

    sdk[; client=<name>[/<version>]][; title=<title>][; url=<url>]; ua=python/<V> httpx/<V>
    

    Values must be printable ASCII excluding ;, and app_name / app_version
    additionally exclude /, since they are joined as <name>/<version>.
    Invalid values raise ValueError at construction time, before any transport
    is 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:

    leading sdk Names the channel. The You.com MCP server emits mcp in the same position, so a single lowercase token keeps the two consistent.
    ua= Runtime of the calling process, e.g. 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= Identifies the caller, not the SDK. Dropped entirely when app_name is unset, so an undeclared caller sends sdk; ua=python/…. The SDK's own name and version travel in the User-Agent.
    title= / url= Human-readable name and website for the calling application.

    A wrapping integration can therefore be distinguished from direct SDK use:

    sdk; ua=python/3.11.15 httpx/0.28.1                       ← direct SDK use
    sdk; client=<integration>/<version>; ua=python/…          ← via an integration
    sdk; client=acme-bot/2.4.0; title=Acme Bot; ua=python/…   ← declared application
    

    Integrations that override sdk_configuration.user_agent should append
    rather 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.com
    MCP server, which is the only layer that can populate its flags accurately.
    A negative test pins this.

  • safesearch parameter on You.answer().
    New optional safesearch keyword argument on answer() / answer_async()
    accepting off, moderate (default), or strict. Case-insensitive. Brings
    the 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. On
3.1.1 the package root carried 48 public names; 24 were import machinery
rather than API and are gone: httpx, asyncio, warnings, weakref, the
typing 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 importable
directly 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 *. BackoffStrategy is newly resolvable from the root
alongside RetryConfig.

The version dunders (__version__, __title__, __user_agent__,
__openapi_doc_version__) are deliberately not in __all__, so a star
import cannot overwrite a consumer package's own __version__. They remain
reachable as attributes.

MIGRATION.md gains a 3.1.1 → 3.1.2 section covering this and the
evt.event.value typing change.

Verification

  • pytest (excluding live and mockserver-dependent tests): 302 passed.
  • Live API: attribution and answer suites pass against production, so the new
    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 CI
    gate): 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 independently
    confirms safesearch on /v1/answer matches the live contract.
  • SSE tests pass under -W error::UserWarning (zero serialization warnings).

Tests added

  • tests/test_root_init.py — transport-free import invariant (asserted in a
    subprocess), import * binds every __all__ name and the sub-packages are
    usable, and import * does not clobber a consumer's __version__.
  • tests/test_attribution.py — wire-format grammar, validation (segment
    forgery, header injection, non-ASCII, control characters, / in
    client= halves), construction-time fail-fast, caller http_headers
    override precedence, direct-use vs integration separation, keyword-only
    signature, X-MCP-Attribution never sent (sync and async), and version
    resolution through importlib.metadata including the uninstalled-checkout
    fallback.
  • tests/test_researchtaskstreamevent.py — known and unknown event
    round-trips, the declared-type contract, and a regression test driving the
    real stream_research_task SSE decode path with unknown event names
    (verified to fail with ResponseValidationError against the pre-fix code).
  • tests/test_live.pyX-Client-Info sent and accepted on real requests,
    X-MCP-Attribution absent, answer(safesearch=STRICT).

Release artifacts

  • pyproject.toml + src/youdotcom/_version.py bumped to 3.1.2
  • uv.lock auto-bumped
  • CHANGELOG.md ## [3.1.2] - 2026-08-20 entry
  • MIGRATION.md 3.1.1 → 3.1.2 section
  • README.md ### Attribution subsection, safesearch in the answer example
  • USAGE.md [attribution] example block
  • docs/models/answerrequestbody.md safesearch row
  • docs/models/eventname.md (new), docs/models/event.md open-enum note,
    docs/models/researchtaskstreamevent.md retyped to models.EventName
  • docs/sdks/you/README.md answer parameter table rebuilt in the
    house-standard format with Response / Errors sections

🤖 Generated with Claude Code

@factory-droid

factory-droid Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

@factory-droid

factory-droid Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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 stream_research() yields raw dicts). The new answer(safesearch=...) surface looks correct, but consider advertising models.SafeSearch directly in the method type hints for IDE/type-checker discoverability.

Comment thread src/youdotcom/sdk.py Outdated
# 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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

Comment thread src/youdotcom/sdk.py Outdated
] = None,
country: Optional[Union[str, models.Country]] = None,
language: Optional[Union[str, models.Language]] = None,
safesearch: Optional[str] = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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).

Comment thread README.md
@@ -69,6 +69,7 @@ A synthesized answer with citations, grounded in live web results.
res = you.answer(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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).

Comment thread README.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

Comment thread MIGRATION.md Outdated
> `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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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().

Comment thread USAGE.md Outdated
```

`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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

@factory-droid

factory-droid Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

@factory-droid

factory-droid Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

@factory-droid

factory-droid Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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 app_version docstring so it matches the constructor’s ValueError behavior. The README snippet self-containment point was rejected here because it is already covered by an existing review comment.

Comment thread src/youdotcom/sdk.py Outdated
Comment on lines +237 to +239
: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 ``/``).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

Suggested change
: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 ``/``).

@factory-droid

factory-droid Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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.

Comment thread README.md
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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"):
    break

@factory-droid

factory-droid Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Validated the candidate comments, both are accurate and actionable. Main follow-ups are fixing case-insensitive X-Client-Info overrides in http_headers, and documenting answer()'s per-call timeout_ms/http_headers in the You SDK docs table.

Comment thread src/youdotcom/basesdk.py
# 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

Comment thread docs/sdks/you/README.md
| `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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

@factory-droid

factory-droid Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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.

Comment thread tests/test_root_init.py
pass ``-S``: that flag disables the venv's ``site.py`` shim and
would render the SDK uninstalled for the subprocess.
"""
result = subprocess.run(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

@factory-droid

factory-droid Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

@factory-droid

factory-droid Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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 X-Client-Info’s generated ua= segment sanitization.

Comment on lines +177 to +179
httpx_version = str(getattr(httpx, "__version__", "unknown"))
if not httpx_version.isascii() or any(c in httpx_version for c in ";/"):
httpx_version = "unknown"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

Suggested change
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"

Comment thread docs/sdks/you/README.md
| `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. | |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

Comment thread MIGRATION.md Outdated

| 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) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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.

Suggested change
| 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>
@factory-droid

factory-droid Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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>
@tyler5673
tyler5673 marked this pull request as ready for review August 21, 2026 06:42
@factory-droid

factory-droid Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job



Review summary: No additional high-confidence, actionable issues found beyond existing PR comments.

(Inline comments posted: 0)

@tyler5673
tyler5673 merged commit 1f7fc6e into main Aug 21, 2026
7 of 8 checks passed
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.

1 participant