Skip to content

Release v3.0.0: Answer API, Speakeasy removal, flattened API surface - #32

Merged
tyler5673 merged 35 commits into
mainfrom
release/v3.0.0
Aug 6, 2026
Merged

Release v3.0.0: Answer API, Speakeasy removal, flattened API surface#32
tyler5673 merged 35 commits into
mainfrom
release/v3.0.0

Conversation

@tyler5673

@tyler5673 tyler5673 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

youdotcom 3.0.0

Adds the Answer API, removes the Agents API, and flattens the sub-SDK layer into direct methods on You. The SDK is also off code generation now: hand-maintained, with scripts/check_drift.py catching what the generator used to prevent.

Major bump because of the removals. Supersedes #31, which was numbered 2.6.0.

Upgrade impact

Change Affects Action
Agents API removed Callers of you.agents* Pin youdotcom<3, or call POST /v1/agents/runs directly
search_helpers, you.search_post() removed Callers of either Use you.search()
__gen_version__, SPEAKEASY_GENERATOR_VERSION removed Anything reading generator metadata Use __version__
Empty api_key_auth raises os.getenv("YDC_API_KEY", "") Drop the "" default
Context managers close both transports Sync and async calls on one instance One instance per flavor
search(language=None) sends no language Explicit language=None Omit the argument to keep EN

Everything else in this release is additive. you.search.unified() and you.contents.generate() continue to work behind shims that emit DeprecationWarning.

Architecture

One class, direct methods

The sub-SDK layer is gone. Search, ContentsSDK, Agents, and Runs were each BaseSDK subclasses reached through a lazy-import mechanism on You: a _sub_sdk_map, a dynamic_import() helper with its own retry-on-KeyError loop, and __getattr__ / __dir__ overrides to make the attributes appear. A call spent three hops getting to the HTTP client, and the machinery defeated static analysis on the way.

# 2.5.x
you.search.unified(query=..., language=models.Language.EN, include_domains="a.com,b.com")

# 3.0.0
you.search(query=..., language="en", include_domains=["a.com", "b.com"])

Every operation is now a method on You. The dynamic import machinery is deleted outright, signatures take plain strings instead of enum imports, and domain lists are lists rather than comma-joined strings. Async is a real method with a real signature, not a **kwargs forwarder. The old spellings survive as thin deprecation shims that translate arguments and delegate.

Hand-maintained, with drift detection

Dropping Speakeasy means dropping the guarantee that the SDK matches the published spec. That guarantee is worth replacing rather than losing, so scripts/check_drift.py now diffs endpoints, server URLs, enum values, request parameters, and response fields against the live OpenAPI documents on every PR and weekly on a schedule. Codegen enforced conformance by construction; this enforces it by observation, and reports rather than rewrites.

Breaking changes

Agents API removed

you.agents(), you.agents_async(), the you.agents.runs sub-SDK shim, and all associated model and error classes are deleted. The endpoint itself is unaffected. Only SDK support is withdrawn.

Empty API key is rejected at construction

# 2.5.x, silently fell through to YDC_API_KEY / YOU_API_KEY_AUTH
You(api_key_auth=os.getenv("YDC_API_KEY", ""))

# 3.0.0, ValueError naming the likely cause
You(api_key_auth=os.getenv("YDC_API_KEY"))   # None requests the env lookup

Every endpoint requires a key, so "" was never a meaningful argument. It indicates a key was expected and did not arrive. Rationale for rejecting rather than ignoring is under Review notes.

Callables are resolved lazily and therefore validated on first use.

Context managers release both transports

You instantiates a sync and an async transport. Previously __exit__ closed only the sync client and __aexit__ only the async one, leaking whichever the block did not use. Both now close both.

# No longer valid: the transports are closed on block exit
with You(api_key_auth=key) as you:
    you.search(query="...")
await you.search_async(query="...")

Caller-supplied transports are still never closed by the SDK.

search(language=...)

Call 2.5.x 3.0.0
you.search(query=q) EN EN
you.search(query=q, language="fr") FR FR
you.search(query=q, language=None) EN field omitted

There was previously no way to issue an unfiltered query.

Added

Answer API

res = you.answer(query="What caused the 2008 financial crisis?")
res.answer                    # markdown with inline [[1, 2]] citations
res.citations[0].source       # source URL
res.citations[0].excerpts     # supporting excerpts
res.results.web               # results used during synthesis

POST /v1/answer on api.you.com, sync and async, with freshness, country, language, and the three domain controls. Full error coverage including 402.

PaymentRequiredResponseError

First-class 402 handling with the quota fields parsed out: upgrade_url, limit, used, period, reset_at.

Case-insensitive enum parameters

country, language, safesearch, livecrawl, livecrawl_formats, and freshness accept plain strings in any case and normalize to the casing the API expects. Enum members continue to work. This behavior was already documented; it is now implemented. The same path corrects the separator in uppercase date ranges (2026-01-01TO2026-02-01 becomes ...to...).

Expanded 422/500 models

UnprocessableEntityResponseErrorData gains detail (FastAPI) and errors (JSON:API) alongside error; InternalServerErrorResponseData gains errors. All observed response shapes now deserialize. Additive.

Drift detection

scripts/check_drift.py, described under Architecture. Non-blocking on every PR, scheduled weekly, and it files or updates a single tracking issue when it finds something.

Security

Debug logging emitted the API key in plaintext. Authorization, X-API-Key, Cookie, and Set-Cookie are now redacted before request and response headers reach the logger, on both transports.

Debug logging is off by default, since get_default_logger() returns a NoOpLogger unless YOU_DEBUG is set, so a default configuration never wrote these values anywhere. The exposure was limited to callers who deliberately enabled debug output, either through YOU_DEBUG or by passing their own debug_logger. For those callers the key appeared in plaintext in whatever sink the logger was wired to. If that describes your deployment and those logs left the host, rotate the key.

Fixed

  • Teardown could mask the caller's exception. A failing client close inside __exit__ propagated in place of whatever exception was leaving the with block, so the user saw a teardown error instead of their own. Now guarded, consistent with close_clients.
  • SDKConfiguration.retry_config had a non-functional default. The field used pydantic.Field(default_factory=...) on a stdlib @dataclass, which does not interpret FieldInfo and left the descriptor itself as the default. Now dataclasses.field.
  • Security.serialize_model never matched its own field. optional_fields contained "ApiKeyAuth" against a field named api_key_auth, so a None key was serialized rather than omitted.
  • _populate_from_globals compared strings by identity. is not in place of !=, correct only while the strings happen to be interned.
  • Async methods were untyped. search_async() and contents_async() were **kwargs: Any wrappers, erasing their signatures for type checkers and IDEs. They are now the implementations, with explicit parameters. This also resolved the one pylint error in the tree.
  • Test transports leaked. 72 unclosed sockets across the suite. Ownership moved to the shared test factories, and filterwarnings plus a gc.collect() in teardown now fail the build on a regression.

Internal

Speakeasy removal is complete: all generation banners, YDCUserAgentOverrideHook, _hooks/registration.py, overlays/python_overlay.yaml, and the .speakeasy/ gitignore entries. __user_agent__ derives from the resolved __version__. Custom user agents set via sdk_configuration.user_agent continue to be honored.

Dead code removal takes mypy's footprint from 105 source files to 80: agent models and errors, 36 orphaned doc files, and Go handlers for agents and GET /v1/search.

The README is rewritten for a hand-maintained SDK, 663 lines down to 403, organized by capability rather than by mechanism. A LICENSE file is committed for the first time.

CI

  • Mock server readiness is polled against /_mockserver/health and fails the job on timeout, instead of proceeding into misleading test failures.
  • Dev tooling installs from the pyproject dependency group rather than a duplicated list in the workflow. The two had already diverged: pyright is declared in the group but was never installed or run in CI.
  • pylint gates rather than running under continue-on-error. --enable=E covers the error tier, undefined names and bad call signatures and unreachable code, which is the same class mypy already gates on, and a step that cannot fail cannot enforce it. The check pays for itself here: the untyped-async defect under Fixed surfaced as E1125.
  • The drift workflow updates its existing issue instead of filing a duplicate weekly, renders its output correctly, and distinguishes a broken checker (exit 3, job fails) from genuine drift (exit 1) and a transient fetch failure (exit 2). Previously an internal error exited 1 and would have been filed as drift.
  • dependabot moved from the pip ecosystem to uv, which is what actually maintains uv.lock.
  • Matrix widened to Python 3.10 through 3.13, and concurrency added.

Verification

Unit + mock server tests 249 pass (154 before)
Performance tests 29 pass
mypy clean, 80 source files
pylint 10.00/10
Drift check clean against live specs
Matrix 3.10, 3.11, 3.12, 3.13

New coverage targets what previously shipped untested: header redaction, empty-key rejection, dual-transport teardown and its exception-masking property, and the normalization and language matrices including the deprecated shim path.

Every README code example was executed against a mock transport rather than eyeballed, and every documentation link was resolved with a control request to confirm the docs host returns 404 for bad paths rather than an SPA shell.

Review notes

Three items are judgment calls rather than mechanical fixes, and are the ones worth scrutiny:

  1. Rejecting an empty API key is a deliberate behavior change. The alternative, treating "" as an opt-out, was considered and rejected: no endpoint supports unauthenticated access, so the only reachable outcome was a 401, and an immediate ValueError names the defect better than a delayed HTTP error.
  2. Closing both transports on either exit fixes a leak but ends mixed sync/async use of a single instance. Documented in MIGRATION with the supported pattern.
  3. Gating on pylint is safe only while the tree stays clean. The <5 pin bounds new checks to a dependabot PR rather than an unrelated feature branch.

tyler5673 and others added 20 commits August 5, 2026 15:20
### Added
- Answer API: you.answer() / you.answer_async() for POST /v1/answer.
  Returns synthesized markdown answers with inline citations and web results.
- PaymentRequiredResponseError: first-class 402 error class for the
  answer API (UpgradeRequiredResponse schema).

### Changed
- Flattened API surface: sub-SDK chains (you.search.unified(),
  you.contents.generate()) are deprecated with DeprecationWarning and
  delegate to new direct methods (you.search(), you.contents()).
  Plain strings accepted instead of enum imports for country, language,
  safesearch, livecrawl, freshness.
- Removed all Speakeasy generated code and disclaimers. SDK is now
  fully hand-maintained.
- Removed YDCUserAgentOverrideHook (no-op after Speakeasy removal).
  __user_agent__ derived from __version__ at runtime.
- search_helpers module merged into you.search() direct method.
- 422/500 error data models expanded with optional detail/errors fields
  (backward compatible).
- Dev dependencies updated: mypy >=2.3.0, pylint >=4.0.0,
  pytest >=9.0.0, pytest-asyncio >=1.0.0.
- CI: replaced pylint/pyright with mypy, excluded live tests from CI.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… empty api_key_auth opt-out

- Fix broken MIGRATION.md anchor link (#250-to-260 → #250--260)
- CONTRIBUTING.md: add --ignore=tests/test_performance.py to unit test command
- Security: explicit empty api_key_auth now constructs Security(api_key_auth=None)
  instead of falling back to env vars (prevents confused-deputy footgun)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…t_live.py

- Delete .agents/skills/generate-sdk-and-open-pr/SKILL.md — was a Speakeasy
  generation workflow, no longer relevant since SDK is hand-maintained
- Remove empty .agents/ directory tree
- Fix stale 'overlay-generated' comment in test_live.py

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Keyless search never shipped in a release, so referencing a revert
is misleading. Renamed heading and header to describe the current
state without implying users experienced keyless.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Remove 'Hand-maintained additions' section from CHANGELOG 2.4.0
  (references deleted registration.py, removed YDCUserAgentOverrideHook,
  and Speakeasy regen workflow that no longer exists)
- Remove .speakeasy/ entries from .gitignore
- Fix stale 'overlay' references in examples/api-example-calls.py,
  MIGRATION.md, tests/test_research.py
- Fix stale 'reverted overlay' comment in tests/test_research.py
- Fix stale 'Speakeasy regen' comment in tests/test_security_env.py
- Clean up Speakeasy reference in CHANGELOG 2.4.0 Fixed section

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The POST /v1/search endpoint was already supported in 2.5.x.
The actual change is search_helpers.search() became you.search().

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…example paths

- Fix SDKConfiguration: use dataclasses.field instead of pydantic.Field
  (stdlib @DataClass doesn't interpret pydantic.Field)
- Fix tests/README.md: replace broken 'pip install -e ".[dev"]' with
  'pip install -e . mypy pylint pyright pytest pytest-asyncio'
- Fix PERFORMANCE_TESTING.md: document pytest-xdist requirement for -n auto
- Fix examples/api-example-calls.py: venv name (.venv not venv) and
  run path (python examples/api-example-calls.py)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… not

'is not' is identity comparison, not equality — can silently fail
for strings depending on interning, causing globals-based overrides
to be skipped.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Same fix as CONTRIBUTING.md — performance tests need timing
infrastructure and are slow, not suitable for CI unit test run.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Hit /_mockserver/health (returns 200) with retry loop instead of
  broken 'curl -sf / || echo ready' that always printed ready
- Remove -x flag so CI shows all failures, not just the first one

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- scripts/check_drift.py: fetches OpenAPI specs from you.com and compares
  endpoints, server URLs, and enum values against the SDK surface
- .github/workflows/test.yml: non-blocking drift check on every PR
- .github/workflows/drift-check.yml: weekly scheduled check that opens
  a GitHub issue if drift is detected, closes stale issues when clean

Known uncovered APIs (billing, images) and endpoints (GET /v1/search)
are excluded via exception lists.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- New handler: tests/mockserver/internal/handler/pathpostv1answer.go
- Register route in generated_handlers.go
- Add TestAnswerMockServer class with 4 tests hitting the mock server
  (basic, freshness, boost_domains, async)
- Existing MockTransport tests retained for error case coverage
- Update tests/README.md test count (19 -> 23)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- drift-check.yml: add explicit permissions (contents: read, issues: write)
  so GITHUB_TOKEN can create/close issues in scheduled runs
- check_drift.py: use context manager for httpx.Client to guarantee
  cleanup on exception paths

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Drift check (check_drift.py):
- Add request parameter drift: compares spec request body properties
  against SDK method signatures
- Add response schema drift: compares spec 200 response schema fields
  against SDK pydantic model fields
- Fix  resolution for request bodies
- Fix array response schema handling (contents returns List[])

Real drift found and fixed:
- ResearchResponse missing 'warnings' field (List[str]) per OpenAPI spec
- Added to model and TypedDict, backward-compatible (Optional, defaults None)

CI improvements (test.yml):
- Add Python 3.11 and 3.13 to test matrix (was 3.10 + 3.12 only)
- Add pytest-cov coverage reporting (--cov=youdotcom --cov-report=term-missing)
- Add pylint errors-only check (non-blocking, continue-on-error)
- Add package build check job (python -m build --sdist --wheel)

Dependabot (.github/dependabot.yml):
- Weekly checks for pip and github-actions dependency updates

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… drift workflow

P1: You.__aexit__ now closes SDK-owned sync client too; __exit__ closes
SDK-owned async client (prevents socket leaks in async-only usage)

P2: Fix Security.serialize_model() optional_fields — was 'ApiKeyAuth'
but actual field key is 'api_key_auth' (no alias), causing null fields
to be re-introduced despite exclude_none

P2: Drift workflow now distinguishes 'DRIFT DETECTED' from 'ERROR:'
before opening an issue (prevents false drift issues on network failures)

P2: Search docs — removed stale GET recommendation and changed
UsageSnippet method from get to post

P2: Document test client cleanup expectations in create_test_http_client

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
P1: Fix __exit__ async client close under running event loop —
use get_running_loop() + run_coroutine_threadsafe() pattern
(matching close_clients()) instead of asyncio.run() which raises
RuntimeError in notebooks/async contexts

P1: Clarify server_url constructor docstring — search/contents
default to ydc-index.io and are not affected by the constructor
server_url unless the per-method server_url argument is passed

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- CHANGELOG: Remove 'Search/Contents host' from Changed (was already
  the case in 2.5.0, not a new change in 2.6.0)
- CHANGELOG: Reword 'search() server_url fix' to accurately state
  that constructor server_url does NOT affect search/contents
  (only per-method server_url is respected)
- README: Fix error count from 27 to 28 (3 httpx + 25 YouError subclasses)
- MIGRATION: Add note that server URL behavior was already the case
  in 2.5.0, documented for reference
- tests/mockserver/README: Replace stale 'auto-generated code from
  Speakeasy' with 'hand-maintained', remove deleted pathgetv1search.go
  from handler list, add all current handler files
- PR description: Remove 'Search/Contents host split' from Breaking
  Changes (was already the case in 2.5.0)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
FinanceResearchEffort no longer includes a LITE tier (only DEEP and
EXHAUSTIVE per official You.com docs). Removed stale 'lite' bullet
from docstrings in finance_researchop.py (2 places) and sdk.py
finance_research/finance_research_async (2 places).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
P1: Fix drift-check.yml re-execution TOCTOU race — capture first run's
output once via GITHUB_ENV instead of re-running the script a second
time. Use distinct exit codes (0=no drift, 1=drift, 2=fetch error)
instead of grepping free-form stdout, eliminating the silent-loss
fallback where neither DRIFT DETECTED nor ERROR: appeared.

P1: Fix test_answer.py resource leaks — test_async_answer was creating
a real httpx.AsyncClient() (no MockTransport) that opened TCP sockets
to the mock server and was never closed. Switched to MockTransport
pattern (matching the rest of the file) with explicit try/finally
cleanup. Also added client.close() to the 3 sync mock-server tests
that were leaking create_test_http_client instances.

P2: Fix check_drift.py request param schema extraction — was using
bare schema.get('properties', {}).keys() while the response path used
_get_schema_properties() which handles oneOf/anyOf/allOf and arrays.
Unified to use _get_schema_properties for both paths. Also removed
redundant inline 'import re' in fetch_specs() (already at module level).

P2: Add upper bounds to pyproject.toml dev deps — mypy, pylint, pytest,
pytest-asyncio were unbounded across semver-major bumps. Pinned upper
bounds (<3, <5, <10, <2) so Dependabot can't float untested majors.

P3: Add failure sentinel to test.yml mock-server readiness loop — if
all 10 retries fail, the loop now exits 1 with a clear error message
instead of silently falling through to pytest.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Addresses the review findings on the v2.6.0 release PR.

Versioning:
- Bump 2.6.0 -> 3.0.0. The Agents API, `search_post()`, `search_helpers`,
  and the sub-SDK classes are removed, which is breaking and needs a major
  version under the SemVer policy the CHANGELOG claims to follow.

Behavior:
- An empty `api_key_auth` now raises ValueError at construction instead of
  silently falling back to the environment. Every endpoint requires a key,
  so `""` is never valid -- it means a key was expected and none arrived,
  nearly always `os.getenv("YDC_API_KEY", "")` with the variable unset.
  Falling back there ran the request under a different identity than the
  code asked for. Callables are checked on first use.
- `search(language=None)` sends no language again. Omitting the argument
  still uses the API default (EN); previously an explicit None also
  resolved to EN, with no way to opt out of the filter.
- `__exit__`/`__aexit__` no longer let a failing client close replace the
  exception propagating out of the block, matching `close_clients`.
- `country`/`language`/`safesearch`/`livecrawl`/`livecrawl_formats`/
  `freshness` are now genuinely case-normalized, which the docs already
  claimed. Also fixes the `to` separator in uppercase date ranges.
- `search_async()`/`contents_async()` are real methods with full
  signatures instead of `**kwargs: Any` wrappers that erased them for type
  checkers and IDEs. Clears the one pylint error in the tree.

Tests (154 -> 249):
- New coverage for debug-log redaction, empty-key rejection, dual client
  close and its non-masking property, and the normalization matrix
  including the deprecated `unified()` path. Redaction and the key
  handling were both shipped untested.

Docs:
- Replace all 52 `os.getenv("YDC_API_KEY", "")` examples, which the new
  key handling turns into an error.
- CHANGELOG gains Removed/Security/Fixed sections; drops an entry for a
  file that never existed on main and two no-op entries. Security is now
  redaction alone -- the key change is fail-fast, not a closed hole.
- MIGRATION gains an action-required table plus sections on key
  resolution, client lifecycle, redaction, and `language`.

CI:
- Drift issues update an existing open issue instead of filing a duplicate
  weekly; body written via --body-file so the code fence renders; a broken
  checker exits 3 and fails the job rather than reporting "no drift"
  forever; --strict and --verbose mean something again.
- Dev tooling installs from the pyproject dependency group instead of a
  duplicated list. pylint gates now that the tree is clean. dependabot
  switched pip -> uv so uv.lock actually gets updated.

Verified: 249 tests pass, mypy clean (80 files), pylint 10.00/10, drift
check green against the live specs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Debug logging is off by default (NoOpLogger unless YOU_DEBUG is set), so
the previous wording overstated the blast radius. State the precondition,
the default, and the one action that matters for anyone who was affected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
Comment thread tests/test_research.py Outdated
Comment thread tests/test_research_helpers.py Outdated
Addresses two P1 review comments on #32: two async tests built an
httpx.AsyncClient, handed it to You(async_client=...), and never closed
it. The SDK deliberately never closes caller-supplied transports, so
those clients leaked. Both now use `async with httpx.AsyncClient(...)`.

The tests added earlier in this branch had the same defect, so they are
fixed too rather than leaving the pattern half-applied:

- test_redaction.py, test_security_env.py: close the mock-backed client
  in a finally.
- test_param_normalization.py: fold client ownership into a `_capture`
  context manager, which also removes four copies of the same handler
  boilerplate.
- test_client_lifecycle.py: back every client with a MockTransport. The
  `_Boom*` clients raise on close by design and so can never be disposed
  of normally; with no connection pool there is nothing to leak.

All four files now run clean under `-W error::ResourceWarning`.

Not addressed here: 25 pre-existing unclosed clients in test_research.py
and others, none of which this PR touches (confirmed against origin/main).
They are the same defect class and worth a follow-up, but folding a
mechanical sweep of untouched files into a release PR would obscure the
release diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread README.md Outdated
Comment thread MIGRATION.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread tests/test_answer.py Outdated
Comment thread tests/test_direct_methods.py Outdated
Comment thread tests/test_shims.py Outdated
tyler5673 and others added 2 commits August 5, 2026 23:44
Follows the two P1 review comments to their conclusion: the suite had 72
unclosed sockets, not 2. All are now closed, and CI enforces it.

Where they were: test_research.py (27), test_performance.py (29),
test_contents.py (12), test_research_helpers.py (4). Tests built on
httpx.MockTransport have no connection pool and never leaked, which is why
the count is far below the ~110 client constructions in the suite.

Rather than 72 call-site edits, ownership moves to the shared factories.
create_test_http_client() and create_timing_client() register what they
build, and an autouse fixture in the new tests/conftest.py closes the
registry after each test. Three edits cover 68 sites and every future
caller. The remaining 4 were inline async clients and are now wrapped in
`async with`.

The guard: filterwarnings promotes ResourceWarning and
PytestUnraisableExceptionWarning to errors. Both are needed — a leaked
transport surfaces during finalization, which pytest reports as an
unraisable exception rather than a failure. The gc.collect() in the
fixture is load-bearing for the same reason: without it, finalization
happens at an arbitrary later point, often after the session, where it
fails nothing. Verified by deliberately leaking a socket and confirming
the run errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The empty-key row read 'a callable returning one', which pointed back at
the non-empty row above it and inverted the meaning. Say 'a callable
returning an empty string'. Applies to README, MIGRATION, and CHANGELOG.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
Test resource leaks (5 comments):
- test_answer.py: converted _sync_you/_async_you helpers to context
  managers (@contextmanager/@asynccontextmanager) that close the
  httpx.Client/AsyncClient in finally. Updated all 19 call sites to
  use with/async with blocks.
- test_direct_methods.py: same context manager conversion for
  _sync_you/_async_you. Updated all 8 call sites.
- test_shims.py: converted _you helper to @contextmanager. Fixed 2
  async tests to use try/finally with aclose().
- test_research_helpers.py: converted TestStreamResearchTypedErrors
  _sync_you/_async_you to context managers. Added explicit close/
  aclose calls to 34 inline-client tests that were leaking.
- test_research.py: already addressed (all async tests already use
  async with httpx.AsyncClient(...)).

README SSE example (1 comment):
- Replaced you.stream_research_task() example with stream_research()
  helper from research_helpers. The strict pydantic decoder in
  stream_research_task raises ResponseValidationError on undocumented
  intermediate event names; stream_research uses a tolerant decoder.

API key resolution table (3 comments):
- README/MIGRATION/CHANGELOG: already addressed — the empty-key row
  already says 'a callable returning an empty string', not the
  contradictory 'a callable returning one'.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job



Review summary

The PR is a large, well-structured v3 surface refactor with drift-checking and expanded test coverage, and no high-confidence correctness or security issues stood out in review. One README warning line still contradicts the new credential-redaction behavior for debug logging.

Comment thread README.md Outdated
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job



Review summary

Overall the v3.0.0 refactor looks solid and well-tested. One concrete correctness bug remains in query param serialization merging, and two small hardening items are worth addressing (clearer lifecycle error when reused after close, and percent-encoding path parameters to prevent path injection).

Comment thread src/youdotcom/utils/queryparams.py
Comment thread src/youdotcom/basesdk.py
Comment thread src/youdotcom/utils/url.py
tyler5673 and others added 3 commits August 6, 2026 01:38
… path param encoding

- queryparams.py: Fixed extend(value) → append(value) for serialized
  query params. _get_serialized_params() returns dict[str, str], so
  extend() was treating the string as an iterable of characters,
  corrupting the query param list. (P1)
- basesdk.py: Added 'if client is None: raise ValueError' guard in
  _build_request_with_client() before calling client.build_request().
  Mirrors the existing guard in do_request/do_request_async. Prevents
  AttributeError when reusing an SDK instance after __exit__/__aexit__.
  (P2)
- url.py: Percent-encode path params with urllib.parse.quote(value,
  safe=) before substituting into URL templates. Prevents path
  injection when params contain /, ?, #, or ... Applied to both
  generate_url() and template_url(). (P2 security)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The file was still shaped by Speakeasy: 663 lines, 28 generated-section
markers, a hand-maintained table of contents, and the first API call at
line 139 behind four package managers, a uv shebang tutorial, and a
PyCharm plugin note.

Compared against the Tavily, Exa, and Parallel Python SDKs. The first two
organize by capability and reach a working call within ~15 lines; Parallel
is reference-shaped like ours because it is Stainless-generated. For a
search product competing with the first two, capability-first is the right
genre. Restructured accordingly: Install, Quickstart, one section per API,
Async, Long-running research, then Authentication, Errors, and
Configuration below the fold. 403 lines, first call at line 32.

Corrections found while checking claims against the code:

- Retries: the old text said the SDK "will fall back to the default retry
  strategy provided by the API". It does not retry at all unless given a
  RetryConfig (sdkconfiguration.retry_config defaults to UNSET).
- Maturity: said breaking changes could land without a major version bump,
  which contradicts the SemVer policy this release is built on. Replaced
  with a Versioning section.
- The lead example used `language=models.Language.EN`, arguing against the
  plain-string normalization this release added, and ended in `print(res)`
  dumping a whole model.
- Answer, the headline 3.0.0 feature, had no example anywhere in the body.
- The 28-class error list is now a status-by-endpoint table, generated
  from the actual raise sites in sdk.py.
- The research background helpers, previously mentioned only in passing,
  now have their own section.

Verified: every code example executed against a mock transport, every
signature checked against the source, every link resolved (with a control
404 to confirm the docs host is not returning SPA shells for bad paths).
No pricing figures — the plans page is linked instead.

The license badge is dropped rather than carried over; see the PR comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repository had no license text committed, even though the README has
always declared MIT. Adds the canonical SPDX MIT text, verified
word-identical to the SPDX register apart from the copyright line.

Also moves pyproject off the deprecated `license = { text = ... }` table to
the PEP 639 SPDX expression plus `license-files`, so the built metadata
carries `License-Expression: MIT` rather than a free-text field — the
machine-readable form license scanners and SBOM tooling consume. Verified
the file ships in both artifacts: `youdotcom-3.0.0/LICENSE` in the sdist,
`dist-info/licenses/LICENSE` in the wheel.

Also fixes an ambiguity in the new README's auth table: the empty-key row
said "a callable returning one", which pointed back at the valid-key row
above it — the same wording the review bot flagged in MIGRATION and
CHANGELOG.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tyler5673 and others added 2 commits August 6, 2026 02:05
The README rewrite kept the redaction note but dropped the operational
guidance added in b54d003 — don't enable debug logging in production, and
don't commit debug logs. Both belong: headers are redacted, bodies are not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every docs.you.com URL 301s to you.com/docs, so the short host was costing
a redirect on each link. Two paths had also gone stale beyond that:

- /search/search-operators -> /guides/search-operators (moved)
- the docs root -> /welcome

Switched to the canonical host throughout and to the current
search-operators path, which now resolves in zero hops instead of two.
Also covers the two generated doc pages and the SearchRequestBody
docstrings, which carried the old URL into user-facing help text.

Left as-is: you.com/docs and /docs/api-reference each keep one landing
redirect, which is how the docs site routes its entry points.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@factory-droid

factory-droid Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Review summary

Overall the v3.0.0 refactor looks solid and well-scoped. One CI-blocking issue stands out: the workflow’s dependency-group install command is missing a target, which is likely to fail before tests run.

@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
@youdotcom-oss youdotcom-oss deleted a comment from factory-droid Bot Aug 6, 2026
Comment thread .github/workflows/test.yml Outdated
Replace separate 'pip install -e .' and 'pip install --group dev' with
a single 'pip install --group dev -e .' so pip has the project context
needed to read dependency groups from pyproject.toml.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Droid finished reviewing PR #32.


Review summary

The v3.0.0 refactor looks solid and well-tested. One small documentation issue remains: the README debug logging snippet is missing an import logging.

Posted 1 inline comment.

Comment thread README.md Outdated
The debug logging snippet used logging.getLogger() without importing
the logging module, causing NameError on copy-paste.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Droid finished reviewing PR #32.


Review summary

No candidate inline comments were provided for Phase 2 validation, so nothing was posted. At the current head SHA, no high-confidence, actionable correctness or security issues remain outstanding in this PR.

Posted 0 inline comments.

tyler5673 and others added 2 commits August 6, 2026 09:21
Prose pass over README, MIGRATION, and CHANGELOG: 75 em and en dashes
removed, rewritten as real clauses rather than swapped for commas, since
most were bolting an afterthought onto a finished sentence. Table cells
using an em dash as a "not applicable" marker became `n/a`.

Three fixes from an independent demo built solely off the README:

- The Configuration examples constructed `you = You(...)` bare, with no
  context manager and no cleanup, while Resource Management said to use
  one. Since there is no public `close()`, those examples modelled a leak.
  All four now use `with`, and the custom-client example closes the
  transport it supplies.
- Reworded "close it when you're done" in Resource Management, which
  implied a `close()` method that does not exist.
- Timeouts: with no `timeout_ms`, requests inherit httpx's 5 second
  default (basesdk falls back to `httpx.USE_CLIENT_DEFAULT`). The answer,
  research, and finance_research endpoints take far longer than that, so
  the Quickstart as written would have raised ReadTimeout. Confirmed
  against a real socket server rather than MockTransport, which ignores
  timeout extensions. The Quickstart now sets `timeout_ms`, and the
  Timeouts section leads with the constraint.

Every README example re-executed against a mock transport after the edits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The timeout note landed between the example and the sentence explaining it,
splitting the two, and the section had two notes in different formats. Both
now sit together after the code, as one blockquote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tyler5673
tyler5673 merged commit f4522b4 into main Aug 6, 2026
8 of 25 checks passed
@factory-droid

factory-droid Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Droid finished @tyler5673's task —— View job


Droid finished reviewing PR #32.


Review summary

No candidate inline comments were provided for Phase 2 validation, so nothing was posted. At the current head SHA, no high-confidence, actionable correctness or security issues remain outstanding in this PR.

Posted 0 inline comments.

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