Python SDK 2.4.0 (research background, finance_research, source_control, output_schema) - #14
Python SDK 2.4.0 (research background, finance_research, source_control, output_schema)#14tyler5673 wants to merge 3 commits into
Conversation
Adds Finance Research API, Research background mode (GET poll / SSE stream), Research source_control and output_schema, Search boost_domains, and Contents max_age. Breaking (with documented migration): - New 'FinanceResearchEffort' enum (DEEP, EXHAUSTIVE) joins existing 'ResearchEffort'. Both names preserved cleanly via named-component refs in the OpenAPI specs (no 'ResearchResearchEffort' doubling). - livecrawl_formats now strictly typed as Optional[List[LiveCrawlFormats]]. - 'you.research()' response is now 'Union[ResearchResponse, TaskResponse]' (TaskResponse returned when background=True). - 'output.content' is now 'Union[str, object]' for output_schema responses. - Shared 401/403/422 error shapes consolidated to named '*ResponseError' classes (per-endpoint classes still raised). 'workflow.yaml' references 'https://you.com/specs/openapi_finance_research.yaml' even though that URL is not yet live (frontend PR youdotcom #12477 is unmerged). A regen from public URLs is therefore expected to produce a diff against this commit until #12477 lands. The SDK source under 'src/' contains all 2.4.0 work as of this commit. Two spec-side fixes on #12477 make this regen match cleanly when it lands: - 'ResearchEffort' and 'FinanceResearchEffort' promoted to named components (avoids Speakeasy disambiguator doubling the names). - 'ResearchTaskStreamEvent.event' enum widened to include the server's synthetic fallback names ('completed', 'failed') for stale RabbitMQ streams, in addition to the documented 'connected', 'response.done', 'complete', 'error', 'cancelled'. Post-generation work (hand-maintained, not regenerated): - src/youdotcom/research_helpers.py adds research_background[_async], poll_research_task[_async], research_and_wait[_async], and tolerant stream_research_events_raw[_async] helpers for background-mode workflows. - src/youdotcom/_hooks/registration.py's YDCUserAgentOverrideHook now respects custom 'sdk_configuration.user_agent' instead of unconditionally rewriting the header. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- CHANGELOG: reword output_schema bullet to honestly document the extra="ignore" limitation + sync-fallback workaround (was claiming structured dict which the generated Content model drops) - CHANGELOG: add pydantic <2.13 defensive pin note to Notes section - research_helpers: soften stream-mode deadline comment in sync + async variants — acknowledge httpx read timeout is the real backstop for stalled SSE connections, not timeout_s - README: fix invalid Python in per-request-retry sample (RetryConfig positional after kwargs → retries=RetryConfig(...)) - SKILL: refine Step 4i-9 overlay example with Speakeasy docs link, x-speakeasy-jsonpath: rfc9535, and corrected JSONPath targets Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Review summaryNicely scoped 2.4.0 release: Finance Research API, research background mode + task polling/SSE streaming, Findings [HIGH] 1. Async SSE helper crashes on exit — [MEDIUM] 2. Test-coverage gap on the async streaming path — [LOW] 3. SDK title mislabeled as Finance Research API — Minor / non-blocking
Everything else (poll/terminal-state logic, |
| # contextlib.aclosing guarantees the underlying httpx SSE response is | ||
| # closed deterministically on consumer break/return/throw, rather than | ||
| # relying on garbage-collected aclose() of the suspended async generator. | ||
| stream = await _open_raw_stream_async( | ||
| client, task_id, http_headers=http_headers, from_id=from_id, | ||
| ) | ||
| async with contextlib.aclosing(stream) as closing_stream: | ||
| async for evt in closing_stream: | ||
| yield evt |
There was a problem hiding this comment.
Bug: contextlib.aclosing calls aclose(), which EventStreamAsync does not implement — this raises AttributeError on every clean exit.
contextlib.aclosing(thing) calls await thing.aclose() in its __aexit__. But EventStreamAsync (in utils/eventstreaming.py) only defines an async close() method — there is no aclose(). So when this async with block exits (stream fully consumed, or consumer break/return), it will raise:
AttributeError: 'EventStreamAsync' object has no attribute 'aclose'
This breaks stream_research_events_raw_async() (the error surfaces to the caller once the stream ends) and the cleanup path of research_and_wait_async(mode="stream"). The sync counterpart (stream_research_events_raw, line 407) is fine because it uses with _open_raw_stream(...) and EventStream implements __exit__.
The comment on 483–485 claims aclosing "guarantees the underlying httpx SSE response is closed deterministically" — in practice it throws instead of closing.
Suggested fix — drop aclosing and close the stream explicitly via the method it actually exposes:
| # contextlib.aclosing guarantees the underlying httpx SSE response is | |
| # closed deterministically on consumer break/return/throw, rather than | |
| # relying on garbage-collected aclose() of the suspended async generator. | |
| stream = await _open_raw_stream_async( | |
| client, task_id, http_headers=http_headers, from_id=from_id, | |
| ) | |
| async with contextlib.aclosing(stream) as closing_stream: | |
| async for evt in closing_stream: | |
| yield evt | |
| # Close the underlying httpx SSE response deterministically on consumer | |
| # break/return/throw. EventStreamAsync exposes async close() (not aclose), | |
| # so a try/finally is used rather than contextlib.aclosing. | |
| stream = await _open_raw_stream_async( | |
| client, task_id, http_headers=http_headers, from_id=from_id, | |
| ) | |
| try: | |
| async for evt in stream: | |
| yield evt | |
| finally: | |
| await stream.close() |
Please also add an async streaming test (see the coverage note in the summary) — this would have caught it.
| ) | ||
| result = hook.before_request(hook_ctx, request) | ||
| assert result.headers.get("User-Agent") == "youdotcom-temporal/0.1.0" | ||
|
|
There was a problem hiding this comment.
Test coverage gap on the async streaming path. There are async tests for research_background_async and poll_research_task_async (incl. error paths), but nothing exercises stream_research_events_raw_async or research_and_wait_async(mode="stream"). That's precisely the untested code that hides the contextlib.aclosing/aclose() AttributeError bug flagged in research_helpers.py. Please mirror TestStreamResearchEventsTolerant and TestResearchAndWaitStreamMode with async variants (an httpx.MockTransport on an httpx.AsyncClient works the same way). While here, a small test for the mode validation (research_and_wait(mode="bogus") → ValueError) would be cheap coverage too.
| ## Summary | ||
|
|
||
| You.com API: Unified API for Express, Advanced, and Custom Agents from You.com | ||
| You.com Finance Research API: Unified API for Express, Advanced, and Custom Agents from You.com |
There was a problem hiding this comment.
Doc accuracy: the summary now leads with "You.com Finance Research API: Unified API for Express, Advanced, and Custom Agents…", which mislabels the whole SDK — it covers Agents, Research, Search, and Contents, not just Finance Research. This flows from info.title being changed to You.com Finance Research API in .speakeasy/out.openapi.yaml. Consider restoring the top-level title to You.com API (in the source spec / overlay) so the regenerated README summary stays accurate.
|
Thanks for the thorough second pass. All findings addressed in the latest commit: [HIGH] 1. Async SSE aclosing bug — fixed. Replaced [MEDIUM] 2. Async streaming test coverage — added. New [LOW] 3. SDK title mislabeled — fixed. Hand-edited the README summary block ("You.com Finance Research API" -> "You.com API") and added a new overlay action ( Minor items:
Test results: 18/18 MockTransport-based tests pass (4 new). Pylint 10.00/10. Server-dependent tests skipped (mockserver not running). |
…anup) HIGH: Fix contextlib.aclosing crash in stream_research_events_raw_async - EventStreamAsync has close() not aclose(), so aclosing.__aexit__ raised AttributeError on every clean exit - Replaced with try/finally: await stream.close() - Removed unused import contextlib MEDIUM: Add async streaming test coverage - TestStreamResearchEventsTolerantAsync (mirrors sync tolerant stream) - TestResearchAndWaitStreamModeAsync (mirrors sync stream mode) - TestModeValidation (mode='bogus' -> ValueError, sync + async) - _AsyncChunks helper for AsyncClient + MockTransport streaming LOW: Fix SDK title mislabeled as Finance Research API - Hand-edited README summary block (You.com Finance Research API -> You.com API) - Added overlay action targeting info.title -> You.com API Minor: Move deadline computation to stream-only path (was unused in poll mode for both sync and async variants) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
| async for evt in stream_research_events_raw_async(client, task.task_id): | ||
| name = evt.event | ||
| if name in {"response.done", "complete"}: | ||
| return await poll_research_task_async( | ||
| client, | ||
| task.task_id, | ||
| interval_s=interval_s, | ||
| timeout_s=max(interval_s, deadline - time.monotonic()), | ||
| ) |
There was a problem hiding this comment.
Minor resource-cleanup nuance: returning out of this async for on response.done/complete does not deterministically run the finally: await stream.close() inside stream_research_events_raw_async. Unlike sync generators (finalized promptly by refcounting), an async generator's aclose() is only scheduled by the event loop's async-gen finalizer / GC, so the SSE HTTP connection can stay open past this return until then.
The comment in stream_research_events_raw_async says contextlib.aclosing was avoided because EventStreamAsync exposes close() (not aclose()) — but you can wrap the async generator itself (which does implement aclose()) here to force deterministic teardown:
import contextlib
async with contextlib.aclosing(
stream_research_events_raw_async(client, task.task_id)
) as events:
async for evt in events:
...Not blocking (tests pass and the httpx read-timeout is an eventual backstop), but worth tightening given the module already goes out of its way for deterministic cleanup. Same applies to the sync path, though refcounting makes it far less of a concern there.
|
Review summary Reviewed with focus on the manually-maintained surface ( Verified
Note: I could not execute the test suite in this review environment (command sandboxing), so the above is static verification. Worth confirming before merge
Minor / non-blocking
Nice work on the "Hand-maintained additions" list in the CHANGELOG — explicitly flagging what must be re-applied after the next Speakeasy regen is exactly right for a generated SDK. |
Summary
Release Python SDK 2.4.0. Adds the new Finance Research endpoint, Research background-mode helpers (poll/stream),
search_postandretrievetyped methods, structured output viaoutput_schema, source_control domain constraints, and Content API cache freshness. Plus the post-generation hand-edits that close gaps between Speakeasy output and what 2.4.0 needs to ship as a clean, tested release.What's new in 2.4.0
you.finance_research(input=..., research_effort=FinanceResearchEffort.DEEP|EXHAUSTIVE)— finance-optimised index (earnings releases, SEC filings, analyst coverage).you.research(..., background=True)returns a typedTaskResponsedirectly. Newyou.get_research_task(task_id)for polling,you.stream_research_task(task_id)for SSE.source_control:include_domains,exclude_domains,boost_domains,freshness,country(beta).output_schema: structured JSON output inresponse.output.content(beta;litereturns 422).boost_domains: per-request domain boost that doesn't restrict (POST + GET viayou.search.unified).max_age: filter cached content freshness.*UnprocessableEntityError/*UnauthorizedError/ etc.Hand-maintained code (NOT regenerated by Speakeasy)
These need re-application after future regens until the overlay in
overlays/python_overlay.yamlcauses Speakeasy to bake them into the generated output:src/youdotcom/research_helpers.py—research_background[_async],poll_research_task[_async],research_and_wait[_async],stream_research_events_raw[_async]. Models background-mode research flow that the generated SDK can't model cleanly (typed TaskResponse narrowing + poll/stream helpers + tolerant SSE decoder).src/youdotcom/_hooks/registration.py—YDCUserAgentOverrideHookhonorssdk_configuration.user_agentoverrides so integrations (langchain-youdotcom,youdotcom-temporal,n8n-nodes-youdotcom) can identify their traffic.src/youdotcom/utils/security.py—get_security_from_envreadsYDC_API_KEY(canonical peryou.com/docs) withYOU_API_KEY_AUTHas fallback for 2.3.x users. Covered bytests/test_security_env.py.Post-generation hand-edits to generated output
All justified in the skill's new Step 4i-9 (and the regeneratable ones will be re-applied after the next regen via the overlay):
CHANGELOG.md— full 2.4.0 release notes.MIGRATION.md— 2.3.0 → 2.4.0 section: env var rename, Union[str, object] content, ResearchEffort enum rename (no rename here, child enums added), enhanced search domain constraints.examples/api-example-calls.py— every example fixed to compile + use real model names + return the right shapes (the Speakeasy-generated copy hadResearchResearchEfforttypos, wrong isinstance, wrong boost_domains type, and amodel_dump()call that prints nothing).README.md/USAGE.md— lead search examples in both stopped combininginclude_domains + exclude_domains + boost_domains(returned 422); now show onlyexclude_domains + boost_domainswhich is the only valid pair.ContentandResultmodels insrc/youdotcom/models/researchresponse.pyandtaskdetail.pyareclass Content(BaseModel): pass/class Result(BaseModel): pass, which silently drop structured payload via pydanticextra="ignore". Workarounds documented inCHANGELOG,MIGRATION, helper docstring, examples, and tests. Durable fix staged inoverlays/python_overlay.yaml(see below).Durable fix staged for next regen
overlays/python_overlay.yaml(bumped to 1.1.0) now contains two actions verified against.speakeasy/out.openapi.yaml:Per Speakeasy's
additionalPropertiessemantics, the nextspeakeasy runshould turn these anonymous object schemas intoextra="allow"Python models. Once we regen and verify with the check in the skill's Step 4i-9 (grep "extra=\"allow\"" src/youdotcom/models/researchresponse.py src/youdotcom/models/taskdetail.py), the workaround caveats in CHANGELOG / MIGRATION / examples / helper docstring / tests get swept up to the clean story in a follow-up commit.Skill update
.agents/skills/generate-sdk-and-open-pr/SKILL.mdStep 4i grew from 3 to 9 sub-steps to capture every post-generation fix we ended up applying this release:ydc-index.io)extra="ignore"data-loss risk)Future maintainers regen'ing the SDK should walk through 4i-1 through 4i-9 before shipping.
Tests
research(background=True)TaskResponse shape,get_research_taskTaskDetail shape,output_schemacontent_type flip with caveat lock-in,source_control(include only + boost+exclude pair), helpers — async error paths forpoll_research_task_asyncandstream_research_events_raw_async,from_idreconnection parameter, env-var precedence (primary + fallback + override), and background-mode progress streaming.pylint src/youdotcom/clean (10.00/10).~/Workspace/Temp/youdotcom-sdk-smoketest/exercises every SDK call path against the local Go mockserver; all 13 checks pass.Checklist
overlays/python_overlay.yamlhas theadditionalProperties: trueoverlay for the next regen