Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,128 @@ All notable changes to the You.com Python SDK will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.1.2] - 2026-08-20

Sister release track to the extraction-parameter rollout
(3.1.0 + 3.1.1). Every documented surface is backward-compatible; see
"Root namespace narrowing" below for the one exception, which affects
only names that were never part of the public API.

The top-level root surface now resolves lazily via PEP 562
``__getattr__``. `You`, `SDKConfiguration`, `BaseSDK`, `RetryConfig`,
`BackoffStrategy`, the `HttpClient` protocols, the logger and hook
helpers, and the `models` / `errors` / `utils` / `types` sub-packages
all still resolve from the package root, and all still bind under
`from youdotcom import *` — but each is imported on first access, so
``import youdotcom`` no longer pulls transport-layer modules into
``sys.modules``.

### Root namespace narrowing

Replacing the eager `from .sdk import *` / `from .sdkconfiguration
import *` also stops those statements from leaking their own imports
onto `youdotcom`. On 3.1.1 the package root carried 48 public names;
24 of them were import machinery rather than API, and are gone:

- stdlib and third-party modules the SDK imports internally: `httpx`,
`asyncio`, `warnings`, `weakref`
- typing and dataclass helpers: `Any`, `Callable`, `Dict`, `Iterable`,
`List`, `Mapping`, `Optional`, `Tuple`, `Union`, `cast`,
`dataclass`, `field`
- internal plumbing: `eventstreaming`, `get_security_from_env`,
`unmarshal_json_response`, `remove_suffix`
- private submodule aliases: `youdotcom.sdk`, `youdotcom.basesdk`,
`youdotcom.httpclient`, `youdotcom.sdkconfiguration` (still
importable directly, e.g. `import youdotcom.sdk`)

None were documented, exported deliberately, or referenced by any
example. The narrowing is intentional: re-exporting them is what
dragged `httpx` and `urllib.request` into every `import youdotcom`.
If you were relying on one, import it from its own module —
`from youdotcom.utils import eventstreaming`. See
`MIGRATION.md` ("3.1.1 → 3.1.2") for the before/after.

Going the other way, `BackoffStrategy` now resolves from the root
(`from youdotcom import BackoffStrategy`) alongside `RetryConfig`,
which it configures; on 3.1.1 only `RetryConfig` did.

`__version__`, `__title__`, `__user_agent__`, and
`__openapi_doc_version__` remain reachable as attributes
(`youdotcom.__version__`) and, as on 3.1.1, are deliberately **not**
bound by `from youdotcom import *`, so a star import cannot overwrite
a consumer package's own `__version__`.

### 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``, so a Workflow module that does
`from youdotcom.models import SearchResponse` (no
`workflow.unsafe.imports_passed_through()` work-around) prepares
cleanly under the default `SandboxedWorkflowRunner`. PEP 562 module
`__getattr__` mirrors the public-import surface without dragging
transport; the `models/__init__.py` lazy pattern shipped in 3.0.0 was
lifted to the package root. Regression coverage lives in
`tests/test_root_init.py` (subprocess assertion: `import youdotcom`
does not load `httpx` / `urllib.request`).
- **`ResearchTaskStreamEvent.event` accepts future SSE event names.**
`Event` now uses `OpenEnumMeta` so a server-side event-name addition
(a new terminal status, a retry signal, anything the SDK does not
yet enumerate) does not raise `ResponseValidationError` on the
unmarshal path. Known event names still resolve to the `Event` enum
member; unknown names unmarshal as plain `str` values that compare
equal to their raw value, so callers branching on raw strings
(`evt.event == "completed"`) keep working unchanged.
Exhaustive-enumeration callers (`isinstance(evt.event, Event)`) get
the right negative answer. Serialization produces no warnings.
Coverage in `tests/test_researchtaskstreamevent.py`, including a
regression test that drives the real `stream_research_task` SSE
decode path with unknown event names.

The field is declared `EventName` (a new public alias for
`Union[Event, str]`, exported from `youdotcom.models`) because that
is what it holds at runtime. **Typed callers may see a new type
error:** `evt.event.value` no longer type-checks, since the value is
a plain `str` for any event name this SDK version does not
enumerate. That error is the bug surfacing rather than a new
restriction — the same code raises `AttributeError` at runtime the
first time the server emits a new name. Guard with
`isinstance(evt.event, Event)` before using the enum API, or compare
against raw strings, which needs no guard.

Direct coercion also stops validating: `Event("bogus")` now returns the
string rather than raising `ValueError`. That is inherent to an open enum
and applies only to `Event`, whose values the server may extend; the
request-side enums (`SafeSearch`, `Country`, `Language`, ...) stay closed
and still raise.

### Added

- **Attribution header `X-Client-Info` on every outbound request.**
New optional `You(app_name=..., app_version=..., app_title=...,
app_url=...)` constructor args identify the calling application. They are
keyword-only, so later attribution args can be added without a breaking
change; the existing positional parameters are untouched. Wire format:

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

so the analytics layer can distinguish SDK traffic from other
sources. The leading `sdk` token names the channel, matching the `mcp` and
`skill` tokens emitted elsewhere; the calling language stays recoverable
from `ua=`, and the SDK's own version from the `User-Agent`. `client=`
identifies the *caller* and is dropped entirely when `app_name` is unset,
so an undeclared caller emits `sdk; ua=python/… httpx/…`. All four values
must be printable ASCII excluding `;`, with `app_name` / `app_version` also
excluding `/`; invalid values raise `ValueError` at construction time, as
does passing `app_version` without `app_name`.
- **`safesearch` parameter on `You.answer()`.**
The Answer API now supports the same explicit-content filtering
as the Web Search API. New optional `safesearch` kwarg on
`answer()` and `answer_async()` accepts ``off``, ``moderate``
(default), or ``strict``. Case-insensitive, like the search
counterpart. Existing call sites are unaffected.

## [3.1.1] - 2026-08-12

### Fixed
Expand Down
93 changes: 90 additions & 3 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,68 @@ with You(api_key_auth=os.getenv("YDC_API_KEY"), timeout_ms=60_000) as you:
# str(deprecations[0].message) == "livecrawl is deprecated; use extraction instead"
```

## 3.1.1 → 3.1.2

> **Additive release.** No parameter is renamed, removed, or changed in
> meaning. Two changes can require an edit, and only in narrow cases.

### Action required

| 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) |
| `ResearchTaskStreamEvent.event` is typed `Union[Event, str]` | Type-checked code calling `evt.event.value` | Guard with `isinstance(evt.event, Event)`. See [SSE event names](#sse-event-names) |

### Root namespace narrowing

`youdotcom/__init__.py` no longer does `from .sdk import *`, so the names
those statements pulled in transitively are no longer attributes of
`youdotcom`. This is what makes `import youdotcom` transport-free inside a
Temporal Workflow sandbox.

```python
# Before (3.1.1): worked by accident.
from youdotcom import httpx, Optional, eventstreaming

# After (3.1.2): ImportError. Import from the real module.
import httpx
from typing import Optional
from youdotcom.utils import eventstreaming
```

Everything documented still resolves from the root, including under
`from youdotcom import *`:

```python
from youdotcom import You, SDKConfiguration, RetryConfig, BackoffStrategy
from youdotcom import models, errors, utils, types
import youdotcom.sdk # private submodules still import directly
```

### SSE event names

The `event` field on `ResearchTaskStreamEvent` accepts event names this SDK
version does not enumerate, so an added server-side event no longer raises
`ResponseValidationError`. Known names still resolve to `Event` members;
unknown names arrive as plain `str`. The declared type now says so, which
means an unguarded `.value` becomes a type error:

```python
# Type error on 3.1.2 -- and an AttributeError at runtime, on 3.1.1 too,
# the first time the server emits a name this SDK doesn't know.
print(evt.event.value)

# Guarded: correct on both versions.
if isinstance(evt.event, Event):
print(evt.event.value)
else:
print(evt.event)

# Comparing against raw strings needs no guard -- Event members are `str`.
if evt.event == "completed":
...
```

## 2.4.0 → 2.5.0

### New `frontier` Research Effort Tier
Expand Down Expand Up @@ -411,15 +473,40 @@ task = research_background(you, input="...", research_effort=ResearchEffort.DEEP
detail = poll_research_task(you, task_id=task.task_id)

# Option 3: Stream SSE events with a tolerant decoder
# (recommended over you.stream_research_task for real tasks, since
# the server emits intermediate event types not in the strict enum)
# (see the note below on choosing between this and
# you.stream_research_task)
for event in stream_research(you, task_id=task.task_id):
print(event.event, event.data)
if event.event in ("response.done", "complete", "completed"):
break
if event.event in ("error", "failed", "cancelled"):
break
```

> **Note on streaming:** The generated `you.stream_research_task()` method uses a strict pydantic decoder that validates event names against a fixed `Event` enum. The server emits intermediate workflow events (e.g. `response.created`, `response.starting`, `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 `stream_research()`.
> **Note on streaming (as written for 2.5.0):** The generated
> `you.stream_research_task()` method uses a strict pydantic decoder that
> validates event names against a fixed `Event` enum. The server emits
> intermediate workflow events (e.g. `response.created`, `response.starting`,
> `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 instead of crashing. For real research tasks, prefer
> `stream_research()`. (It yields `RawStreamEvent` objects, not raw dicts as
> originally written here; `.event` is a `str` and `.data` is the parsed JSON
> payload.)
>
> **Updated in 3.1.2 — the `ResponseValidationError` half of this no longer
> applies.** `Event` is now an open enum and the field is typed
> `EventName` (`Union[Event, str]`), so `you.stream_research_task()` decodes
> unenumerated event names as plain `str` instead of raising. The three event
> names above are covered by a regression test. See "3.1.1 → 3.1.2" above.
>
> `stream_research()` still differs in which frames it surfaces:
> `stream_research_task()` drops data-less frames (a bare `event: ping`
> heartbeat, or one carrying only `id:`/`retry:`) and requires every frame to
> carry an `id` and a JSON-object `data`, whereas `stream_research()` yields
> them and tolerates a `data` payload that is not valid JSON. If you only
> needed unknown-event tolerance, either method now works.

### Polling and Timeout Guidance

Expand Down
66 changes: 61 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ strings work in any case. `country="us"` and `safesearch="STRICT"` are both
accepted. Elsewhere, pass the value as the API spells it (all lowercase) or
import the enum from `youdotcom.models`.

The snippets below assume the [Quickstart](#quickstart) client is in scope as `you`.

### Answer

A synthesized answer with citations, grounded in live web results.
Expand All @@ -69,6 +71,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).

query="What are the tradeoffs of vector vs. keyword search?",
freshness="month",
safesearch="strict",
include_domains=["arxiv.org"],
)

Expand Down Expand Up @@ -243,14 +246,21 @@ from youdotcom.research_helpers import stream_research

for evt in stream_research(you, task_id=task.task_id):
print(evt.event, evt.data)
if evt.event in ("response.done", "completed", "error", "failed", "cancelled"):
if evt.event in ("response.done", "complete", "completed", "error", "failed", "cancelled"):
break
```

`stream_research()` tolerates event names outside the documented set, yielding
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

event name as a `str` and whose `.data` is the parsed JSON payload (or the raw
string when a frame is not valid JSON, which it tolerates). Since 3.1.2 both
methods tolerate event names outside the documented set — `you.stream_research_task()`
surfaces an unenumerated name as a plain `str` rather than raising — so the
remaining difference is which frames reach you: `stream_research()` yields
data-less frames (a bare `event: ping` heartbeat, or one carrying only
`id:`/`retry:`), whereas `you.stream_research_task()` silently drops them and
requires every frame to carry an `id` and a JSON-object `data`. Prefer
`stream_research()` when you need every frame, including keep-alives. Pass
`from_id` to resume a stream after a disconnect.

Each helper has an `_async` twin: `research_and_wait_async`,
`research_background_async`, `poll_research_task_async`, `stream_research_async`.
Expand Down Expand Up @@ -356,6 +366,52 @@ is the exception: the helpers under
[Long-running research](#long-running-research) manage their own deadlines, so
`timeout_s` there bounds the wait rather than `timeout_ms`.

### Attribution

Every SDK request emits an `X-Client-Info` header so the analytics layer can
split SDK traffic from MCP traffic. The wire format is:

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

The leading `sdk` token names the channel, matching the `mcp` and `skill`
tokens other You.com surfaces emit. The calling language stays recoverable
from `ua=` (`python/…` here, `node/…` from the TypeScript SDK), and the SDK's
own version travels in the `User-Agent` (`youdotcom-python-sdk/<version>`).

Identify your application with `app_name` / `app_version`, which populate the
`client=` segment, and optionally `app_title` / `app_url`:

```python
import os
from youdotcom import You

with You(
api_key_auth=os.getenv("YDC_API_KEY"),
app_name="acme-bot",
app_version="2.4.0",
app_title="Acme Bot",
app_url="https://acme.example",
timeout_ms=60_000,
) as you:
res = you.search(query="...")
```

All four are optional and keyword-only; existing call sites are unaffected.
When `app_name` is
unset the `client=` segment is dropped entirely, so a request from an
undeclared caller is simply `sdk; ua=python/… httpx/…`.

Values must be printable ASCII excluding `;`, and `app_name` / `app_version`
additionally exclude `/` since they are joined as `<name>/<version>`. Passing
`app_version` without `app_name` is an error, since a bare version has nowhere
to go. Invalid values raise `ValueError` at construction time.

The MCP-specific `X-MCP-Attribution` header is never set by the SDK — it is
assembled on the MCP server, which is the only layer that can populate its
`keyless` / `payment` / `ip` flags accurately.

### Servers

`search` and `contents` go to `https://ydc-index.io`. Everything else goes to
Expand Down
39 changes: 38 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,41 @@ The `extraction` parameter replaces the deprecated `livecrawl` /
Unknown keys inside `extraction` raise `ValidationError` locally, and passing
`extraction` together with `livecrawl` / `livecrawl_formats` raises
`ValueError` — both mirror the server's 422 contract so callers fail-fast.
<!-- End SDK Example Usage [extraction] -->
<!-- End SDK Example Usage [extraction] -->

<!-- Start SDK Example Usage [attribution] -->
```python
# Tag every outbound request with a caller-identity header so the
# analytics layer can split SDK traffic from MCP traffic.
import os
from youdotcom import You


with You(
api_key_auth=os.getenv("YDC_API_KEY"),
app_name="acme-bot",
app_version="2.4.0",
app_title="Acme Bot",
app_url="https://acme.example",
timeout_ms=60_000,
) as you:

res = you.search(query="What did OpenAI announce this week?")

# Handle response
print(res)
```

`X-Client-Info` sent on the wire:

```
sdk; client=acme-bot/2.4.0; title=Acme Bot; url=https://acme.example; ua=python/<V> httpx/<V>
```

`app_name`, `app_version`, `app_title` and `app_url` are all optional and
keyword-only. When omitted, those segments are dropped entirely, so an
undeclared caller sends just `sdk; 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>`. Passing
`app_version` without `app_name` is also an error. Invalid
values raise `ValueError` at construction time.
<!-- End SDK Example Usage [attribution] -->
1 change: 1 addition & 0 deletions docs/models/answerrequestbody.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Request body for `POST /v1/answer`.
| `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. |
| `include_domains` | Optional[List[*str*]] | :heavy_minus_sign: | Domains to exclusively include. Cannot combine with `exclude_domains` or `boost_domains`. Max 500. |
| `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. |
Loading
Loading