Skip to content
Closed
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
115 changes: 115 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,121 @@ 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.

### 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.
- **`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
89 changes: 86 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-1) |
| `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

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

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,36 @@ 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
```

> **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 as raw dicts instead of crashing. For real research tasks, prefer
> `stream_research()`.
>
> **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()` is still the better choice for long real tasks, but for a
> different reason: `stream_research_task()` retries `[429, 500, 502, 503, 504]`
> on stream-open, which can silently reopen a half-consumed stream, whereas
> `stream_research()` deliberately skips those retries. It also tolerates data
> frames that are not valid JSON. If you only needed unknown-event tolerance,
> either method now works.

### Polling and Timeout Guidance

Expand Down
57 changes: 53 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ A synthesized answer with citations, grounded in live web results.
res = you.answer(
query="What are the tradeoffs of vector vs. keyword search?",
freshness="month",
safesearch="strict",
include_domains=["arxiv.org"],
)

Expand Down Expand Up @@ -247,10 +248,14 @@ for evt in stream_research(you, task_id=task.task_id):
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 events as raw dicts and tolerates data frames that
are not valid JSON. 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 choose between them on retry behaviour:
`stream_research()` deliberately skips the `[429, 500, 502, 503, 504]` retries
that `you.stream_research_task()` performs on stream-open, which can otherwise
silently reopen a half-consumed stream. Prefer `stream_research()` for long
tasks. 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 +361,50 @@ 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; 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 the analytics side splits `client=` on it.
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
36 changes: 35 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,38 @@ 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. When
omitted, those segments are dropped entirely. Values must be printable ASCII (excluding `;`); invalid
values raise `ValueError` at construction time.
Comment on lines +125 to +127

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

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

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