Python SDK 3.1.2: lazy imports, attribution header, SSE forward-compat, answer safesearch - #48
Python SDK 3.1.2: lazy imports, attribution header, SSE forward-compat, answer safesearch#48tyler5673 wants to merge 1 commit into
Conversation
|
Droid finished @tyler5673's task —— View job Droid review (Phase 2 validation): One concrete fix needed, normalize recorded header keys in the new live attribution test to avoid a casing-related |
| """A real httpx client that records the headers of each request.""" | ||
|
|
||
| def record(request: httpx.Request) -> None: | ||
| observed.append(dict(request.headers)) |
There was a problem hiding this comment.
[P1] Live attribution test can KeyError on header casing
record() stores dict(request.headers) with whatever casing httpx uses (for example X-Client-Info). The test checks for the header case-insensitively but then indexes h["x-client-info"], which can raise KeyError and make the live test flaky.
| observed.append(dict(request.headers)) | |
| observed.append({k.lower(): v for k, v in request.headers.items()}) |
There was a problem hiding this comment.
Fixed in 3fe1dca. Confirmed the failure mode is latent rather than live — dict(httpx.Headers) lowercases today, so the index worked, but the surrounding guard was case-insensitive while the lookup was not, which is self-contradictory and would have turned any future casing change into a KeyError instead of a clean assertion failure.
Took the suggested normalization at record time, and then simplified what it made redundant: the match comprehension is now a plain "x-client-info" in h membership test instead of a case-insensitive check followed by a case-sensitive index, and the MCP-absence check drops its per-name .lower(). Both live tests still pass against prod.
48e2613 to
3fe1dca
Compare
|
Droid finished @tyler5673's task —— View job Droid review (Phase 2 validation): The changes look cohesive and well-tested for the new lazy root surface, attribution header, and SSE forward-compat behavior. The main remaining gap is that the new Event/EventName docs examples are not copy-paste runnable and should be made self-contained per the repo’s documentation contract. |
| ```python | ||
| from youdotcom.models import Event | ||
|
|
||
| if isinstance(evt.event, Event): |
There was a problem hiding this comment.
[P2] Make open-enum example runnable
The “Open enum” snippet references evt without defining it, so copy-pasting fails with NameError; please make the example self-contained (for example, validate a ResearchTaskStreamEvent with a known and an unknown event name) to match the repo’s docs contract that examples are runnable.
There was a problem hiding this comment.
Fixed in 6dff3f4. Both snippets are now self-contained, and I ran every ```python block on both pages rather than eyeballing them — 6/6 execute clean, and the # output comments in the docs are the real captured stdout.
event.md builds a known and an unknown ResearchTaskStreamEvent via model_validate before the isinstance guard; eventname.md imports Union/Event for the alias and constructs a two-frame stream list so the loop has something real to iterate. Good catch — AGENTS.md "Examples must be copy-paste runnable" applies to docs/**/*.md, and the pre-existing Event.CONNECTED example on that page was already honouring it, so mine were the outliers.
| The declared type of `ResearchTaskStreamEvent.event`: | ||
|
|
||
| ```python | ||
| EventName = Union[Event, str] |
There was a problem hiding this comment.
[P2] Make EventName docs example runnable
This page shows Union without importing it and iterates stream without defining it, so copy-paste fails; please include the needed imports and construct a minimal local stream (or show a direct ResearchTaskStreamEvent.model_validate(...) example) so readers can run it as-is.
There was a problem hiding this comment.
Fixed in 6dff3f4. Both snippets are now self-contained, and I ran every ```python block on both pages rather than eyeballing them — 6/6 execute clean, and the # output comments in the docs are the real captured stdout.
event.md builds a known and an unknown ResearchTaskStreamEvent via model_validate before the isinstance guard; eventname.md imports Union/Event for the alias and constructs a two-frame stream list so the loop has something real to iterate. Good catch — AGENTS.md "Examples must be copy-paste runnable" applies to docs/**/*.md, and the pre-existing Event.CONNECTED example on that page was already honouring it, so mine were the outliers.
3fe1dca to
6dff3f4
Compare
|
Droid finished @tyler5673's task —— View job Droid review (Phase 2 validation): Changes look cohesive and well-covered by tests. One doc fix remains, the “Note on streaming” section in MIGRATION.md still reflects the old strict-enum behavior for |
|
|
||
| ### SSE event names | ||
|
|
||
| The `event` field on `ResearchTaskStreamEvent` accepts event names this SDK |
There was a problem hiding this comment.
[P1] Update MIGRATION streaming guidance for open-enum SSE
MIGRATION.md now states unknown SSE event names no longer raise ResponseValidationError as of 3.1.2, but the later “Note on streaming” still claims you.stream_research_task() validates against a fixed Event enum and will raise on intermediate events; please update or qualify that note so the docs match the new open-enum behavior and do not steer users away from stream_research_task for a problem that is now fixed.
6dff3f4 to
9cd40f2
Compare
|
Addressed the streaming-note finding in Verified the claim first.
The recommendation survives, the reason changes. Gates after the change: mypy clean, |
|
Droid finished @tyler5673's task —— View job Droid review (Phase 2 validation): No candidate inline comments to post. After reading the full PR diff, I did not find any additional high-confidence, actionable issues, and there are no security findings in this pass. |
9cd40f2 to
99cd68b
Compare
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
99cd68b to
0fe466b
Compare
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
…t, answer safesearch Implements DX-776, DX-777, DX-778. Fixed - Models are usable inside a Temporal Workflow (DX-776). 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 (DX-778). `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 (DX-777), with optional `You(app_name=..., app_version=..., app_title=..., app_url=...)`. Values must be printable ASCII excluding `;` (and `/` for `app_name` / `app_version`), validated before any transport is constructed. The SDK never emits `X-MCP-Attribution`; that header belongs to the MCP server. The source token is `sdk`, matching the channel-shaped `mcp` / `skill` tokens the MCP server and you-research skill emit, and the only shape that parses under the documented analytics recipe -- a hyphenated token makes the source column empty on every row. The calling language stays recoverable from `ua=` (`python/...` vs `node/...`), which no wrapping integration can override, and the SDK's own version from the `User-Agent`. `client=` identifies the *caller*, not the SDK, and is dropped entirely when `app_name` is unset -- the same rule `buildClientInfoHeader` applies on the MCP server. That keeps the field discriminating: an undeclared caller emits `sdk; ua=python/...`, while an integration emits `client=youdotcom-temporal/1.0.1`, so direct SDK use and first-party integrations are separable structurally rather than by convention. - `safesearch` on `You.answer()` / `answer_async()`. 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/dataclass helpers, internal plumbing (`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, 285 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>
0fe466b to
99ef0c9
Compare
|
Droid finished @tyler5673's task —— View job Droid review (Phase 2 validation): Validated all candidates against the full diff. Posted 3 inline comments for small but real contract mismatches (empty |
|
Superseded by a fresh draft PR. The branch is squashed to a single commit ( |
| if app_version is not None and app_name is None: | ||
| raise ValueError( | ||
| "app_version requires app_name; the attribution header emits " | ||
| "them together as client=<name>/<version>, so a version with " | ||
| "no name has nowhere to go." | ||
| ) | ||
| if app_name is not None: | ||
| validate_attribution_arg("app_name", app_name, forbidden=";/") | ||
| if app_version is not None: | ||
| validate_attribution_arg("app_version", app_version, forbidden=";/") | ||
| if app_title is not None: | ||
| validate_attribution_arg("app_title", app_title) | ||
| if app_url is not None: | ||
| validate_attribution_arg("app_url", app_url) |
There was a problem hiding this comment.
[P1] Reject app_version when app_name is empty
You.__init__ only raises when app_name is None, but build_client_info_header() only emits the client= segment when app_name is truthy, so You(app_name="", app_version="1.2.3") passes validation and then silently drops the version on the wire. Treat an empty app_name as unset in the guard (and align the validation condition) so this misconfiguration fails fast instead of producing an incorrect header.
| if app_version is not None and app_name is None: | |
| raise ValueError( | |
| "app_version requires app_name; the attribution header emits " | |
| "them together as client=<name>/<version>, so a version with " | |
| "no name has nowhere to go." | |
| ) | |
| if app_name is not None: | |
| validate_attribution_arg("app_name", app_name, forbidden=";/") | |
| if app_version is not None: | |
| validate_attribution_arg("app_version", app_version, forbidden=";/") | |
| if app_title is not None: | |
| validate_attribution_arg("app_title", app_title) | |
| if app_url is not None: | |
| validate_attribution_arg("app_url", app_url) | |
| if app_version is not None and not app_name: | |
| raise ValueError( | |
| "app_version requires a non-empty app_name; the attribution header emits " | |
| "them together as client=<name>/<version>, so a version with " | |
| "no name has nowhere to go." | |
| ) | |
| if app_name: | |
| validate_attribution_arg("app_name", app_name, forbidden=";/") | |
| if app_version is not None: | |
| validate_attribution_arg("app_version", app_version, forbidden=";/") | |
| if app_title is not None: | |
| validate_attribution_arg("app_title", app_title) | |
| if app_url is not None: | |
| validate_attribution_arg("app_url", app_url) |
| `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 | ||
| values raise `ValueError` at construction time. |
There was a problem hiding this comment.
[P1] Document the / constraint for app_name and app_version
The attribution example says values must exclude only ;, but the implementation also rejects / in app_name and app_version (since analytics parses client=<name>/<version> on /), so this block is currently out of sync with the real validation contract.
| `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 | |
| values raise `ValueError` at construction time. | |
| `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 `;`), and | |
| `app_name` / `app_version` also exclude `/`; invalid values raise `ValueError` at construction time. |
| """Build the ``X-Client-Info`` header value for outbound SDK requests. | ||
|
|
||
| Emits a caller-identity header so the analytics layer can distinguish | ||
| SDK traffic from other sources. SDK traffic is uniquely identified by | ||
| the leading literal ``sdk``. | ||
|
|
||
| ``build_client_info_header`` is called per-request from | ||
| ``BaseSDK._build_request_with_client`` immediately after the | ||
| ``User-Agent`` header is set. It does no module-level transport | ||
| imports — ``httpx`` is pulled in lazily at the top of the function | ||
| body (as is ``youdotcom`` itself, for the version pin) so that | ||
| ``import youdotcom`` does not regress because of this module. | ||
| """ |
There was a problem hiding this comment.
[P2] Fix attribution module docstring drift
The module docstring says build_client_info_header lazily imports both httpx and youdotcom “for the version pin”, but the implementation only imports httpx and does not inspect youdotcom at all, so the rationale is misleading in a PR that is explicitly about keeping import youdotcom transport-free.
| """Build the ``X-Client-Info`` header value for outbound SDK requests. | |
| Emits a caller-identity header so the analytics layer can distinguish | |
| SDK traffic from other sources. SDK traffic is uniquely identified by | |
| the leading literal ``sdk``. | |
| ``build_client_info_header`` is called per-request from | |
| ``BaseSDK._build_request_with_client`` immediately after the | |
| ``User-Agent`` header is set. It does no module-level transport | |
| imports — ``httpx`` is pulled in lazily at the top of the function | |
| body (as is ``youdotcom`` itself, for the version pin) so that | |
| ``import youdotcom`` does not regress because of this module. | |
| """ | |
| """Build the ``X-Client-Info`` header value for outbound SDK requests. | |
| Emits a caller-identity header so the analytics layer can distinguish | |
| SDK traffic from other sources. SDK traffic is uniquely identified by | |
| the leading literal ``sdk``. | |
| ``build_client_info_header`` is called per-request from | |
| ``BaseSDK._build_request_with_client`` immediately after the | |
| ``User-Agent`` header is set. It does no module-level transport | |
| imports — ``httpx`` is pulled in lazily at the top of the function | |
| body so that ``import youdotcom`` does not regress because of this | |
| module. | |
| """ |
Superseded by #49.
The branch was squashed to a single commit and reopened as a fresh draft so
review could start from a clean slate. See #49 for the current description,
which also reflects a later change to the attribution header's source token.