diff --git a/.agents/skills/generate-sdk-and-open-pr/SKILL.md b/.agents/skills/generate-sdk-and-open-pr/SKILL.md index a9aae7f..93d2c96 100644 --- a/.agents/skills/generate-sdk-and-open-pr/SKILL.md +++ b/.agents/skills/generate-sdk-and-open-pr/SKILL.md @@ -22,6 +22,7 @@ Speakeasy generates the SDK from OpenAPI specs defined in `.speakeasy/workflow.y - `https://you.com/specs/openapi_contents.yaml` - `https://you.com/specs/openapi_base.yaml` - `https://you.com/specs/openapi_research.yaml` +- `https://you.com/specs/openapi_finance_research.yaml` These are merged with the overlay at `overlays/python_overlay.yaml` and output to `.speakeasy/out.openapi.yaml`. @@ -37,6 +38,7 @@ The SDK is generated from these OpenAPI specs: 3. https://you.com/specs/openapi_contents.yaml 4. https://you.com/specs/openapi_base.yaml 5. https://you.com/specs/openapi_research.yaml +6. https://you.com/specs/openapi_finance_research.yaml Are the updates for this release already reflected in these specs, or do you have custom specs to use? ``` @@ -218,7 +220,7 @@ After generation and doc updates, ensure the test suite is compatible with the n #### Test structure - **Unit tests** (`tests/test_runs.py`, `tests/test_search.py`, `tests/test_contents.py`): Run against a Go mockserver in `tests/mockserver/`. These are auto-generated by Speakeasy. The mockserver is started via Docker or the compiled binary. -- **Integration tests** (`tests/test_live.py`): Run against the real You.com API. Require `YOU_API_KEY_AUTH` env var. +- **Integration tests** (`tests/test_live.py`): Run against the real You.com API. Require `YDC_API_KEY` env var. - **Client tests** (`tests/test_client.py`): Test HTTP client setup helpers. #### 4h-1. Update tests for new/changed APIs @@ -243,7 +245,7 @@ If tests fail, fix the test code (or SDK issues if applicable) and re-run. pytest tests/test_live.py -v ``` -If `YOU_API_KEY_AUTH` is not set, skip this step and note it in the PR description. +If `YDC_API_KEY` is not set, skip this step and note it in the PR description. #### 4h-4. Validate tests line by line @@ -256,7 +258,258 @@ After all tests pass, read through every changed test file line by line. Check f If this review surfaces any changes needed, make the fixes and go back to step 4h-2. Repeat this loop until a full line-by-line review finds no additional changes needed. -### 4i. Commit all changes +### 4i. Post-generation manual fixes + +Speakeasy generates the bulk of the SDK automatically, but several known disconnects require manual fixes every release. Go through each item below after generation succeeds. + +#### 4i-1. Fix environment variable name (ALWAYS required) + +Speakeasy generates `YOU_API_KEY_AUTH` as the env var name (derived from `envVarPrefix: YOU` + the security scheme field name `api_key_auth` in `gen.yaml`). The canonical name per `you.com/docs` is `YDC_API_KEY`. + +**Fix**: Edit `src/youdotcom/utils/security.py` in the `get_security_from_env` function: + +```python +# Replace this (generated): +if os.getenv("YOU_API_KEY_AUTH"): + security_dict["api_key_auth"] = os.getenv("YOU_API_KEY_AUTH") + +# With this: +api_key = os.getenv("YDC_API_KEY") or os.getenv("YOU_API_KEY_AUTH") +if api_key: + security_dict["api_key_auth"] = api_key +``` + +`YDC_API_KEY` is primary (canonical). `YOU_API_KEY_AUTH` is kept as fallback for users upgrading from 2.3.x without changing their environment. + +Then bulk-replace `YOU_API_KEY_AUTH` with `YDC_API_KEY` across all non-source files: + +```bash +# Tests, docs, README, USAGE — everywhere except security.py (which has the fallback) +sed -i '' 's/YOU_API_KEY_AUTH/YDC_API_KEY/g' \ + README.md USAGE.md tests/*.py tests/README.md \ + docs/sdks/*/README.md .agents/skills/generate-sdk-and-open-pr/SKILL.md +``` + +Verify only `security.py` retains `YOU_API_KEY_AUTH` (the fallback): + +```bash +grep -rl "YOU_API_KEY_AUTH" --include="*.py" --include="*.md" . | grep -v __pycache__ | grep -v build/ | grep -v examples/ +# Should show only: ./src/youdotcom/utils/security.py +``` + +#### 4i-2. Verify server URLs (do NOT change search/contents URLs) + +The OpenAPI specs for search and contents use `https://ydc-index.io` as the server URL. This is correct and documented at `you.com/docs/api-reference/search/v1-search` (the page explicitly shows `GET https://ydc-index.io/v1/search`). The `api.you.com` host is a free MCP-only proxy (`/v1/agents/search`, 100 searches/day, IP-tracked) — the SDK should NOT use it for search or contents. + +**Verify** (do not change) that these files still have `ydc-index.io`: + +```bash +grep "ydc-index.io" src/youdotcom/models/searchop.py src/youdotcom/models/searchpostop.py src/youdotcom/models/contentsop.py +# All three should show "https://ydc-index.io" +``` + +The base `SERVERS` in `src/youdotcom/sdkconfiguration.py` should remain `https://api.you.com` (used by research, finance_research, agents). + +#### 4i-3. Preserve and verify hand-maintained files + +These files are NOT regenerated by Speakeasy and must survive across regens: + +- `src/youdotcom/research_helpers.py` — background-mode helpers (`research_background`, `poll_research_task`, `research_and_wait`, `stream_research_events_raw`) +- `src/youdotcom/_hooks/registration.py` — `YDCUserAgentOverrideHook` (custom User-Agent support) + +If `speakeasy run` overwrites or deletes these, restore them from git (`git checkout HEAD -- `). + +**Verify the User-Agent hook still works after regen**: The hook in `_hooks/registration.py` compares the configured `user_agent` against `__user_agent__` from `_version.py` (which Speakeasy regenerates) and checks the `speakeasy-sdk/` prefix to detect whether a custom UA has been set. If a future Speakeasy version changes that prefix, the hook's custom-UA detection would silently break. + +```bash +# 1. Verify the hook file was not overwritten +git diff -- src/youdotcom/_hooks/registration.py +# Should show no changes (or only changes you intentionally made) + +# 2. Verify __user_agent__ in _version.py still starts with the expected prefix +grep "__user_agent__" src/youdotcom/_version.py +# Should show: __user_agent__: str = "speakeasy-sdk/python ..." +# If the prefix changed from "speakeasy-sdk/", update _DEFAULT_UA_PREFIX in +# _hooks/registration.py to match + +# 3. Verify the hook is still registered +grep "register_before_request_hook" src/youdotcom/_hooks/registration.py +# Should show: hooks.register_before_request_hook(YDCUserAgentOverrideHook()) +``` + +#### 4i-4. Check for Speakeasy auto-version-bump + +`speakeasy run` may auto-bump the version in `gen.yaml` and `pyproject.toml` beyond what was set in step 4b. If the version was already set correctly, manually revert: + +```bash +# Check if speakeasy changed the version +git diff -- gen.yaml pyproject.toml src/youdotcom/_version.py | grep version +# If the version is wrong, revert to the intended version +``` + +#### 4i-5. Fix pyright/pylint issues in hand-maintained code + +If `research_helpers.py` or other hand-maintained files have type-checker errors after a regen (new generated types may not match old annotations): + +- **pyright**: Use `stream = await _open_raw_stream_async(...)` + `try/finally/stream.close()` instead of `async with _open_raw_stream_async(...)` (pyright treats raw coroutines as non-async-context-manager). Add return type annotations on internal helpers. +- **pylint**: Add `# pylint: disable=protected-access` on functions that access generated internals. Use `yield from stream` instead of `for evt in stream: yield evt`. + +Run both checkers and fix until clean: + +```bash +.venv/bin/pylint src/youdotcom/ --rcfile=pylintrc +.venv/bin/pyright src/youdotcom/research_helpers.py +``` + +#### 4i-6. Verify live test skip condition uses YDC_API_KEY + +`tests/test_live.py` has a skip decorator that checks for the API key env var. Ensure it uses `YDC_API_KEY` (not `YOU_API_KEY_AUTH`): + +```python +@pytest.mark.skipif( + not os.getenv("YDC_API_KEY"), + reason="YDC_API_KEY environment variable not set" +) +``` + +If `YDC_API_KEY` is set in the environment, live tests will run against the real API. To run only unit tests (mockserver-based), exclude live tests: + +```bash +pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v +``` + +#### 4i-7. Run full validation suite + +After all post-generation fixes are applied, run the complete validation: + +```bash +# 1. Start mockserver +cd tests/mockserver && go run . & sleep 3 + +# 2. Unit tests (exclude live + performance) +cd ../.. && .venv/bin/python -m pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v +# Expected: all pass + +# 3. Pylint +.venv/bin/pylint src/youdotcom/ --rcfile=pylintrc +# Expected: 10.00/10 + +# 4. Stop mockserver +kill $(lsof -ti:18080) +``` + +If any check fails, fix and re-run until all pass before committing. + +#### 4i-8. Verify auto-generated Search examples are valid + +Speakeasy assembles per-parameter `example` values into one combined request. For the Search API, three parameters have pairwise mutual-exclusion that the assembled example does not know about: + +- `include_domains` **cannot** be combined with `exclude_domains` (returns `422`). +- `boost_domains` **cannot** be combined with `include_domains` (returns `422`). +- `exclude_domains` + `boost_domains` **is** valid. + +After every regen, grep the lead Search examples in `USAGE.md` and `README.md` (specifically the `` blocks) to confirm no `search_post`/`search.unified` example combines all three of `include_domains`, `exclude_domains`, and `boost_domains`: + +```bash +# Each occurrence with all three is a bug — drop include_domains (keep +# exclude_domains + boost_domains, which is the only valid pair). +grep -nE 'include_domains=\[' USAGE.md README.md +# Expected: no matches. If any are listed, hand-fix by deleting the +# "include_domains=[...]" block (and the comma before it) from each. +``` + +The long-term fix lives upstream: add a request-level `example` block on `SearchRequestBody` / `SearchRequest` in `overlays/python_overlay.yaml` (or the front-end OpenAPI specs) that uses a single valid pair, so Speakeasy prefers that example instead of concatenating per-field ones. Track that as a follow-up; the hand-fix above is what keeps 2.4.0 correct in the meantime. + +Also scan for the `RetryConfig(...)` positional-after-kwargs regression that Speakeasy can produce when `search_post` is the lead example operation: + +```bash +# If you see ", RetryConfig(...)" or similar after a keyword argument in a +# search_post example, the generated Python is a SyntaxError. Fix by +# passing `retries=RetryConfig(...)`. +grep -nE ', RetryConfig\(' README.md USAGE.md +``` + +If anything matches, fix by hand (overlay-up fix is the same follow-up above). + +#### 4i-9. Audit empty-type / open-ended model schemas (`extra="ignore"` data-loss risk) + +When the OpenAPI spec defines a schema with no `properties` (e.g. an empty typed envelope used as `output.content` for `output_schema` requests, or `task.result` for completed background research), Speakeasy emits a `BaseModel` subclass whose body is the literal `pass`: + +```python +class Content(BaseModel): + pass +``` + +Pydantic's default config is `extra="ignore"`, so unknown JSON keys returned by the server are silently dropped at unmarshal. The SDK cannot recover them: `res.output.content.model_dump()` returns `{}`, not the structured dict. Users of `output_schema=` and background-mode research loathe this and have hit it in 2.4.0. + +**Detection** (after `speakeasy run`): + +```bash +# Every BaseModel whose body is just `pass` — these are the silent-drop +# candidates. Read each one alongside the field it backs and decide whether +# the user can recover the data via a different path. If not, fix it. +python3 - <<'PY' +import re, pathlib +for p in pathlib.Path("src/youdotcom/models").glob("*.py"): + for m in re.finditer( + r"^class (\w+)\(BaseModel\):\n((?:[ \t]+.*?\n)+)", + p.read_text(), re.MULTILINE, + ): + if m.group(2).strip() == "pass": + print(f"{p.name}: {m.group(1)}") +PY +``` + +**Spec-side fix (regen-durable).** Add `additionalProperties: true` to the schema in the OpenAPI specification. Speakeasy then generates `extra="allow"` on the resulting model and unknown keys round-trip intact — see the [Speakeasy additionalProperties docs](https://www.speakeasy.com/docs/sdks/customize/data-model/additionalproperties). + +Two ways to land it: + +- **Upstream spec**: Edit the responsible `*.yaml` in `~/Workspace/youdotcom-frontend/public/specs/` and let the next regen pick it up. +- **OpenAPI overlay** (`overlays/python_overlay.yaml`): inject the keyword without touching upstream — survives regens and lives with this SDK. Use the [RFC 9535 JSONPath syntax](https://github.com/speakeasy-api/openapi-overlay) (`x-speakeasy-jsonpath: rfc9535`) to match the existing overlay in this repo: + + ```yaml + overlay: 1.0.0 + x-speakeasy-jsonpath: rfc9535 + info: + title: Allow extras on open-ended response schemas (output_schema content + background task result) + version: 0.1.0 + actions: + # ResearchResponse.output.content has shape oneOf: [string, object] + # where the object branch is anonymous (no `properties`). Speakeasy + # currently emits `class Content(BaseModel): pass` (extra="ignore") and + # silently drops the structured payload returned by the server. + - target: $["components"]["schemas"]["ResearchResponse"]["properties"]["output"]["properties"]["content"]["oneOf"][1] + update: + additionalProperties: true + # TaskDetail.result is an anonymous object schema; same drop behaviour. + - target: $["components"]["schemas"]["TaskDetail"]["properties"]["result"] + update: + additionalProperties: true + ``` + + After applying the overlay and regen, verify the generated model now allows extras: + + ```bash + grep -nE "extra=\"allow\"|class Content\(BaseModel\):" \ + src/youdotcom/models/researchresponse.py + # Expect: from typing import ... ConfigDict ... model_config = ConfigDict(extra="allow") + # (or an equivalent annotation on the Content class) + + # If Content still has `pass` body and no extra="allow" config, the + # overlay didn't apply. Double-check the JSONPath against + # `.speakeasy/out.openapi.yaml`. + ``` + +**Workaround until the fix lands.** When the typed model drops data: + +- `research_helpers.py` docstring + CHANGELOG entry for `research_and_wait` MUST explicitly recommend the synchronous fallback (`client.research(..., background=False)` with the same `input`) and call out that `model_dump()` returns `{}`. +- `MIGRATION.md` `output_schema` example MUST show the same workaround rather than the misleading `output.content["..."]` syntax. +- `tests/test_research.py::TestResearchOutputSchema` MUST lock in `content_type.value == "object"` and the documented model_dump/emtpy-payload behaviour so a careless regen that re-introduces data loss fails loudly. +- Once the spec/overlay fix lands and regen produces `extra="allow"` models, simplify the workaround comments + drop the empty-payload lock-in assertion (replace with one that asserts the round-tripped dict). + +**Long-term.** Treat *empty* typed schemas as a red flag in spec review. Any schema backing a user-facing response field should declare `additionalProperties: true` (or a real schema) — never `{}` / no `properties`. Add a check to the front-end repo's CI (e.g. `scripts/audit-empty-schemas.ts`) so an empty schema in `youdotcom-frontend/public/specs/*.yaml` fails the build with a message pointing to this skill step. + +### 4j. Commit all changes Stage and commit all generated and manually updated files to the release branch: @@ -267,7 +520,7 @@ git commit -m "feat: Python SDK X.Y.Z" Do NOT commit `.speakeasy/workflow.yaml` if it still contains local spec overrides — it should have been reverted in step 4d. -### 4j. Push and open a PR +### 4k. Push and open a PR ```bash git push -u origin release/X.Y.Z diff --git a/.gitignore b/.gitignore index d69301d..df72ff4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ pyrightconfig.json .speakeasy/reports .env .env.local +_debug/ +dist/ diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index 409aa50..e36e746 100644 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -1,41 +1,41 @@ lockVersion: 2.0.0 id: 77dbe0f5-bd71-4808-8102-230d4ffc4dc7 management: - docChecksum: 7d5cd58eeb46bb65394f8d93701de5d3 - docVersion: 1.0.0 - speakeasyVersion: 1.733.4 - generationVersion: 2.845.12 - releaseVersion: 2.3.0 - configChecksum: 8a3f331950b6f7e77a32f7cd1db0b5f8 + docChecksum: 6de7557ad8f3a624326b1e7a852beeb2 + docVersion: 0.0.1 + speakeasyVersion: 1.789.1 + generationVersion: 2.916.4 + releaseVersion: 2.4.1 + configChecksum: 83cf625a098d4086c3be034bd8c6fc56 published: true persistentEdits: - generation_id: 137cb5fb-bdb8-4499-aeff-a6321a1bdb1b - pristine_commit_hash: 6b6328c7225337c31677e43d97830359df07c179 - pristine_tree_hash: bf00b109fbef4edf704a638751b1cd9a7c8e666a + generation_id: 48f81b51-3f1d-4015-a31f-6a389f5b6bcf + pristine_commit_hash: 486edc226ccfca7a104808fc1884f3f06a52ba97 + pristine_tree_hash: 04b3c82fdba3499b7839cd8aa290de5415a66950 features: python: acceptHeaders: 3.0.0 - additionalDependencies: 1.0.0 + additionalDependencies: 1.1.0 constsAndDefaults: 1.0.7 - core: 6.0.15 + core: 6.0.34 defaultEnabledRetries: 0.2.0 - enumUnions: 0.1.0 - envVarSecurityUsage: 0.3.2 + enumUnions: 0.1.1 + envVarSecurityUsage: 0.3.3 flatRequests: 1.0.1 flattening: 3.1.1 - globalSecurity: 3.0.5 + globalSecurity: 3.0.7 globalSecurityCallbacks: 1.0.0 globalSecurityFlattening: 1.0.0 globalServerURLs: 3.2.1 - methodArguments: 1.0.2 + methodArguments: 1.1.1 methodServerURLs: 3.1.2 nameOverrides: 3.0.3 nullables: 1.0.2 responseFormat: 1.1.0 - retries: 3.0.4 - sdkHooks: 1.2.1 - serverEvents: 1.0.13 - unions: 3.1.4 + retries: 3.0.7 + sdkHooks: 1.3.0 + serverEvents: 1.1.1 + unions: 3.1.7 trackedFiles: .gitattributes: id: 24139dae6567 @@ -47,8 +47,8 @@ trackedFiles: pristine_git_object: 8d79f0abb72526f1fb34a4c03e5bba612c6ba2ae USAGE.md: id: 3aed33ce6e6f - last_write_checksum: sha1:9c6e80ea166a9a6d5973a69ce2788e6c65f264c6 - pristine_git_object: 9f08d215c86cda516bb2283683def17f64339774 + last_write_checksum: sha1:d882f687d690476f7ce13e0e6078cdb535e901b8 + pristine_git_object: 428c2a1b6f1b02f052d203cc674e8ab91f3b3e13 docs/errors/agentruns400responseerror.md: id: 4a255bc34dbc last_write_checksum: sha1:52fd0795019408368d8d17c72c7dafe6267e7799 @@ -73,6 +73,46 @@ trackedFiles: id: c673d1fd50f2 last_write_checksum: sha1:c3aae77709d87098770406f004c19a27ef930e83 pristine_git_object: 486b384d58421877ad73fa963a812c8cc83b5f1c + docs/errors/financeresearchforbiddenerror.md: + id: 3ec77ec3445d + last_write_checksum: sha1:34ebb641f33b71361bb5fcb83084f6c71a7ae80d + pristine_git_object: 9e6c3a96816aae16f8bc001ecfff328ca2dc84ac + docs/errors/financeresearchinternalservererror.md: + id: 67bb5aabbed5 + last_write_checksum: sha1:91621cc9156f585fce7c5542e453494652671c12 + pristine_git_object: 0556209616512b5d47ede4930d7d18aa5755059e + docs/errors/financeresearchunauthorizederror.md: + id: 44e3826db8d5 + last_write_checksum: sha1:5d6cf8136cfdcf5ada57360c432f59dc7ab0b96d + pristine_git_object: 2cf887047a9fa831d708594edfda92a01e6180f1 + docs/errors/financeresearchunprocessableentityerror.md: + id: dd6a93d0cd72 + last_write_checksum: sha1:4c7916bbde62fa9bf524a72ae0f0ff6895c5ff19 + pristine_git_object: 470b417ef83776af139e293573137a9fa02ebfa9 + docs/errors/forbiddenresponseerror.md: + id: 7decb227f487 + last_write_checksum: sha1:2704ca01ccd03ee1de3814613057dbac5d662203 + pristine_git_object: 5d49673dfa18a1a6fb21a5c4412d4daaf486e315 + docs/errors/getresearchtaskforbiddenerror.md: + id: e07bbe61e6c4 + last_write_checksum: sha1:281b451466371ca526be3fc62818317e64307602 + pristine_git_object: cc633bb1f53ffa0a36fe9b3b1dea4b99abecd940 + docs/errors/getresearchtaskinternalservererror.md: + id: 81f20dfeefd2 + last_write_checksum: sha1:e060a090e53295548aa077ccb45491ad643d5f7b + pristine_git_object: ebed213bfb4d33a9457fb7ad63e7feeb074909c6 + docs/errors/getresearchtasknotfounderror.md: + id: 6ba7a2c1fa57 + last_write_checksum: sha1:7c55b5870c7c602ecb81c37e36871bb683a2d801 + pristine_git_object: cbb274c9cf9b5cf09081853ca1e2bf17fa3b9039 + docs/errors/getresearchtaskunauthorizederror.md: + id: 0d79362dc977 + last_write_checksum: sha1:41a802256dda83929f9fc516c67ec7608b8cf6e7 + pristine_git_object: 5ed32faf19a4b4e641c94d202abb25108ed19411 + docs/errors/internalservererrorresponse.md: + id: f9d2a251e010 + last_write_checksum: sha1:6699c675081cfa78d7b154a90d02271965b63548 + pristine_git_object: b99d7b0942e2efbe3e202f3ffc92a74f35cd580a docs/errors/researchforbiddenerror.md: id: 38294d725501 last_write_checksum: sha1:6a2a0e75dc6ba751562e90bbb85fda2dda20ff60 @@ -85,38 +125,54 @@ trackedFiles: id: 90348ea8ad14 last_write_checksum: sha1:8948e2a9d19578d31dc9556b3a498208f6eaea1e pristine_git_object: 9516b1732d6641bf0077ceee9c044ef662d456c8 - docs/errors/searchforbiddenerror.md: - id: fb63bcabfad1 - last_write_checksum: sha1:b7e3a2369be34808acaa576f9384b9daf32b9b2c - pristine_git_object: 3a7b4fc18f4f9344cee2250bf78d73a8ae267534 - docs/errors/searchinternalservererror.md: - id: 6aa0b8aa8b2c - last_write_checksum: sha1:3299b61b84ba075aea6830091acc52e96ff121fb - pristine_git_object: 28d41b1c3dc2096be2a879157857e208bed3db44 - docs/errors/searchunauthorizederror.md: - id: bd64fef413da - last_write_checksum: sha1:1d70ee061964342b0e48d2f9f726bbc49d492820 - pristine_git_object: f799351c432b366215f25cff8710c8b082ceb997 - docs/errors/unprocessableentityerror.md: - id: 49570373f8e1 - last_write_checksum: sha1:6995ccac169fa137740bbbf5136a8fd7ee016e15 - pristine_git_object: 5abff965877a345c70cc64d6138b7e84946622f7 + docs/errors/researchunprocessableentityerror.md: + id: 04be881a410c + last_write_checksum: sha1:5b8d4a7da29c0081ce83e2117b1964e369a68da0 + pristine_git_object: 603a7fdb346007af5fafb87bdfc8847bb0021a68 + docs/errors/streamresearchtaskforbiddenerror.md: + id: b68753e29e72 + last_write_checksum: sha1:a6cc16649f92457856ea4f57082be8007e93983f + pristine_git_object: f13d63a3ae2b8b120882494ab91e873b71f34c07 + docs/errors/streamresearchtaskinternalservererror.md: + id: ca67c2dc6fb7 + last_write_checksum: sha1:aaa15cd0eb93dddb20fb632b0181fd8c24b5786b + pristine_git_object: e7d008336aaa801016fbf38beedb6f7d44c25c70 + docs/errors/streamresearchtasknotfounderror.md: + id: 2fb5d2de9d68 + last_write_checksum: sha1:149e9c0a6908dacb57ceb8e3f26360d85c026e44 + pristine_git_object: 8d8ff6b300fd58434ed081a92a0c4b69f22759e1 + docs/errors/streamresearchtaskunauthorizederror.md: + id: a47459aaf050 + last_write_checksum: sha1:0977acbac57ae6b8b3d07b95794c65408b28f462 + pristine_git_object: 86e9ae76f9c539d6804b46e0182ef7371244816b + docs/errors/unauthorizedresponseerror.md: + id: 500194829c27 + last_write_checksum: sha1:29d332afa85c2b7a4d2a8a955eb24010944433ca + pristine_git_object: fe026600dfac122697e638bb3613de731389f638 + docs/errors/unprocessableentityresponseerror.md: + id: cc0e37e6516e + last_write_checksum: sha1:783bebd4bf3453a0e67d63c16e6b9b1f0647631f + pristine_git_object: 5d4449977cb77c2f2265ca82a85f5b6013a22c56 docs/models/advancedagentrunsrequest.md: id: 1b4e175934b4 last_write_checksum: sha1:73a48e950bb5313ee31f36c8bafff77c22078861 pristine_git_object: 596bc50cef57e8402f45539d69327682a047f7e1 docs/models/agentrunsbatchresponse.md: id: 04584f262eed - last_write_checksum: sha1:22c9d779fdccf76ec7b4592630b4b263d6f8e5ce - pristine_git_object: 5922b796b8395ed5f99cc99ce4515bf0c39c1cf3 + last_write_checksum: sha1:6a6790bcf33fe6c07ec9046077e544ff80968d7a + pristine_git_object: 7c345a6026ba4c016dbf7217e6371e6ccf2e7967 + docs/models/agentrunsbatchresponseinput.md: + id: 8edc8ba24b96 + last_write_checksum: sha1:8dad10e437444a93f47de02b060e97f13bdcea3c + pristine_git_object: b15c573cadaf47390bef27d13e3dbbc93a252c09 docs/models/agentrunsresponseoutput.md: id: 628cb50c13b9 last_write_checksum: sha1:b7fe4c0c6d2ea03dd892fc9621b49beb2462cc4d pristine_git_object: b7f92254fcd6c1e10543ef9dba14a33c5310658b docs/models/agentrunsresponsewebsearchresult.md: id: 4a4bd85222f8 - last_write_checksum: sha1:5bc3183d0e6cc770cf55b21fd5d45c549b039cb1 - pristine_git_object: 6bdcf11d3e9fd725f3daf4717f6a3361a2a29665 + last_write_checksum: sha1:204d00c488738a71f8cde4c91470bb64035928fd + pristine_git_object: 95bff3e274cd35e68da02fde008f62a0e7926fe2 docs/models/agentrunsstreamingresponse.md: id: 421588ccf00d last_write_checksum: sha1:b435f704376b951318496e35b1e669ea565c5b88 @@ -133,34 +189,42 @@ trackedFiles: id: 57c817282ef7 last_write_checksum: sha1:e0ebc2c82bc98687d9d3257dff8244e4d1023735 pristine_git_object: 5c8b09feed16bdcfcae523839b5d4c602486e3a7 + docs/models/content.md: + id: bfd859c99f86 + last_write_checksum: sha1:11315e151ff2cdd353c82561563b80f21be6b5c2 + pristine_git_object: 88bdbee7cb9990524ed6b9a0bd161277f246a83e docs/models/contents.md: id: da6e46f8e038 last_write_checksum: sha1:954a223de149c3d7164949ca0783d8ff4616dcdc pristine_git_object: a19143ec48165b836977bc9621ab61c11a52c337 docs/models/contentsformats.md: id: afdc3b11d819 - last_write_checksum: sha1:a34d7a952e3e349c8a098f205e80e328b6dc9fb5 - pristine_git_object: 1944732d23cbe1f44e01d879d907031138e4ab50 + last_write_checksum: sha1:9276989fa981477f876146dc5b0c76482c1a41d4 + pristine_git_object: 8510c9af12a7d962caaf2decdd8cf313179ff018 docs/models/contentsmetadata.md: id: 9240f2df8fbf last_write_checksum: sha1:2e8998401af96080a89aa177135011e8f79be878 pristine_git_object: 6050bc0a757b2ea380ae2b1aaffc789588b44fa0 docs/models/contentsrequest.md: id: 9e0c7be98068 - last_write_checksum: sha1:ee5a86cac6f99df2c46b6a500490dd69d289e194 - pristine_git_object: fec30b007750d47d0d9cb260a0a984fe1d846ba7 + last_write_checksum: sha1:59d78e99358f76e745850cd29b15fd3fcc3ac1ac + pristine_git_object: 847c157730d5d9723d7d4c1fb6b261cd2f73faee docs/models/contentsresponse.md: id: 3be584b2a8cf last_write_checksum: sha1:357146cd86137bee392eed939594c1630937b377 pristine_git_object: 7e81c7aa44ebb89cd11fc8ed4e75ad0802496589 docs/models/contenttype.md: id: 78e9266f4216 - last_write_checksum: sha1:b07de74e6e51de014aac32016de80db12ff83ee3 - pristine_git_object: b043bcf6036d260ace3df072d2dc57d4fdcb4656 + last_write_checksum: sha1:682fb2676600386c8cfb21c1f547d332daa5bf6d + pristine_git_object: 02de4620daff095cebd82650b6c9a37858e3f9e0 + docs/models/contentunion.md: + id: 8e45d9c67f98 + last_write_checksum: sha1:17e8c965731a9d505227aa2b702f9b412879efa5 + pristine_git_object: 4ad69225f8692d5e2b75d2a2bafcacb8a7974849 docs/models/country.md: id: a9be7df1a5df - last_write_checksum: sha1:fe1e2a0d00c9e256751e759eb55ba5e722562be8 - pristine_git_object: a4fc0c4a5fe56d54ac00725850ea1e243b0d8f64 + last_write_checksum: sha1:bf2348c1ad242cd575be6fac84df7d152b75e9af + pristine_git_object: 1bee618806380b9c459faf7df54bd13e832794e7 docs/models/customagentrunsrequest.md: id: 985d655f63bc last_write_checksum: sha1:35757db39efe4a2d68133956ccc14f54a918c561 @@ -173,78 +237,138 @@ trackedFiles: id: bfd4e327e742 last_write_checksum: sha1:32727a92e089117e460fad79af339fe8115a74f4 pristine_git_object: c7ea282bc255e6185466b5769d0e4d83529090db + docs/models/event.md: + id: 311c22a8574a + last_write_checksum: sha1:522f9e6b722f81884211c69da91e4e17ccb79601 + pristine_git_object: c8bc8a7494fb0aee15bef082dbfca4c6f54e2fa5 docs/models/expressagentrunsrequest.md: id: 103f294a3f64 last_write_checksum: sha1:bca1ea4c189cc504768a8eed46e36b2904880bdf pristine_git_object: 271ad1a06da7cc2c5a02c2dcf14091aceaba5fe6 + docs/models/financeresearchcontenttype.md: + id: 251f6709d8fc + last_write_checksum: sha1:4d70e9c48d21c05af08f4ee62023ebe72082f163 + pristine_git_object: eba2d2dc19b8d28b8e5c2a2a703a3bb9cf505fc5 + docs/models/financeresearchdetail.md: + id: da1ea14e56eb + last_write_checksum: sha1:036693ffc8ad02df30ab9f8e63b9b7e2f723c329 + pristine_git_object: ca5a772467f315e72b89d028a80b2dbc5ff9f44b + docs/models/financeresearcheffort.md: + id: 3c7a91a84321 + last_write_checksum: sha1:7f81d8b3d8600921bf4af1a896c2033faef904c4 + pristine_git_object: 9402be19e1f6b23276cdff3ca87188b191d38e52 + docs/models/financeresearchinput.md: + id: e76c0ad8325b + last_write_checksum: sha1:4e72ffd4b153c3f7ff581fa0c9b3a0c0147b1680 + pristine_git_object: c7fc55919d0214e51805b03f3672e6d347f9a83f + docs/models/financeresearchinputunion.md: + id: bd158ae5ecac + last_write_checksum: sha1:453a944df9da5f074deed38023d93d76a0f3228a + pristine_git_object: f6a582f3ecddb8575ab97e27c7f67523b2adf5f3 + docs/models/financeresearchloc.md: + id: 64c17038a446 + last_write_checksum: sha1:e09bf82953f70c283f3d07f90322f8ad5c6f127b + pristine_git_object: df5440326162e805099dc71d6f4e1bec21361c98 + docs/models/financeresearchoutput.md: + id: eca77ec865c2 + last_write_checksum: sha1:f60c7aea6df195364fef1e36c0fc4ea4e5e72641 + pristine_git_object: b6e352829b15f4137713cc3dde8273caad295fcd + docs/models/financeresearchrequest.md: + id: 4fa7a1dfd720 + last_write_checksum: sha1:9600c4303006d36d246f578294e8b494bdaef6c2 + pristine_git_object: 7342a4ffeedc6c5fa7cdb36a0cc95bd7f21d1156 + docs/models/financeresearchresponse.md: + id: 4d259e737b06 + last_write_checksum: sha1:7c9245e636fd48185b4837af1e3d7a77d00c3d2d + pristine_git_object: 7db9645ec29a80418b2734ef7fde43701ea70681 + docs/models/financeresearchsource.md: + id: ba4fa6623cd8 + last_write_checksum: sha1:b1f2807cbadb88c549f1971800883ea4de888624 + pristine_git_object: 90a8e2a8ed3d62ff906b2e699ec8cc3cc55fcc60 docs/models/freshness.md: id: 88acdb1a4cde - last_write_checksum: sha1:5a20b012ce095064aaedfc09db5dbdf170ffc27c - pristine_git_object: e3b507d2b9e37f660ba541d2d4967ae2db681434 - docs/models/input1.md: - id: 13ac5c7b8e41 - last_write_checksum: sha1:cd17fe02727f29475456bd76bfd5dd2ecf61612d - pristine_git_object: 8b6df1b3fe78557e96f42e4c72b02a32d3b7c8e8 - docs/models/input2.md: - id: f914f15b24f1 - last_write_checksum: sha1:4e59087bf04e435ff5ec762ecf80e3a2abd490f6 - pristine_git_object: fefc4e562d59c1e90c2c3886d6c1735be5d88a9d + last_write_checksum: sha1:f55d65e3cd1f3a40eb8f43e64390112bec8674dd + pristine_git_object: 38377ef33d46910d8f1cd3d2d759d5b833bc37cb + docs/models/freshnessvalue.md: + id: a0ab09970422 + last_write_checksum: sha1:ff17988d9c2ce0ca2217247d227b7f6d1d454c7c + pristine_git_object: 4c5c0585ee55b19f51feea0f726761b3303fc694 + docs/models/getresearchtaskrequest.md: + id: b474bd70b4b4 + last_write_checksum: sha1:18822518b262e485e79077d8e3f4a873fa2662fd + pristine_git_object: ba0a7b2aa0ad60efef4fac6028f0e8da542d0407 docs/models/language.md: id: 5bac2bb42c7c - last_write_checksum: sha1:06df16a4f694c3844bfcd18e3f44e23b3f49efc9 - pristine_git_object: 862060bbb008c512f27d50fc792a74ae49d47de9 + last_write_checksum: sha1:ccb522cc80826dbb4aa2583df981c3a30ad2d6f1 + pristine_git_object: b8d19c1c9f8a36551caa305ba70eb257e97b20df docs/models/livecrawl.md: id: 8f56dc6b7dc1 - last_write_checksum: sha1:0fa766715e42797a2b53a6c3f905d171c007c8e4 - pristine_git_object: ff4239adeea3c3273d4d2b77ee98be032782e79c + last_write_checksum: sha1:262a55ae586cbf797d208ab335f3d5505a4c0f89 + pristine_git_object: c62c4ee26a56f1183589c591d0dbcbdacd08161d docs/models/livecrawlformats.md: id: e1f2ecc26149 - last_write_checksum: sha1:d5f25c6bd17417afa054143b342d82099cccb005 - pristine_git_object: dcfb11252321864cc52c4761d626f702c5ef88d8 + last_write_checksum: sha1:e4da6638be2cab806a029d2fd186a5dd2b5a4c48 + pristine_git_object: c2d0a6b5c3c6ba084aaf0fd105f4618483bb4c25 docs/models/loc.md: id: b071d5a509cc last_write_checksum: sha1:09a04749333ab50ae806c3ac6adcaa90d54df0f1 pristine_git_object: d6094ac2c6e0326c039dad2f6b89158694ef6aa7 - docs/models/metadata.md: - id: ad63dba5a4f8 - last_write_checksum: sha1:aea47d78e0d9e08c5a4709833dc6426334c82503 - pristine_git_object: 66efc64ae136f21939a01251b2bf89ad216e3bfb - docs/models/news.md: - id: c070462247dc - last_write_checksum: sha1:f5aea8d54b1d4f21cd98d934ae913ecdecd35a6b - pristine_git_object: fec0835c5d443cb3675a49ff221152b17b6bb027 + docs/models/newsresult.md: + id: 6faa8e42dd7f + last_write_checksum: sha1:bd70dd78f300f80a0ef8f64623657685a630e14a + pristine_git_object: 0aad594860f49247b39b19cc210080776c39ca45 docs/models/output.md: id: 376633b966cd - last_write_checksum: sha1:45d7d4baf3d5bd47998470a3261d5d842216a895 - pristine_git_object: d7c2771a56efeb0f63d0c388c621748c5ac93572 + last_write_checksum: sha1:64cef92ef8a82fd875b10643aac8b1af7a729109 + pristine_git_object: a47cad9ce6c60f15f1b2bdf5206457d75420d280 + docs/models/outputschema.md: + id: 48e0d37a531a + last_write_checksum: sha1:597f2357558ae2127685db3f3df80386d1b5cc7f + pristine_git_object: 44443ec9f0eec032c27947bba313062916d74579 docs/models/reportverbosity.md: id: d06de274a3c5 - last_write_checksum: sha1:a9b998858b5efb701cfee75690c1a0e160ffdce1 - pristine_git_object: 1a2aae5cdb01b31eb1590b76e52f08b9ab6d19f0 + last_write_checksum: sha1:9b7dce8c7da103c20041cc87e52de081401d245f + pristine_git_object: 7c3d17fc8487c0ebe15ea35e754895b2a8531f00 docs/models/researchdetail.md: id: ac4ed946dccb - last_write_checksum: sha1:f43174f11eb01588ded19f3e06c072a3589c4df9 - pristine_git_object: e635556b70c8aff5211a13aab43c6aa27fad4b4e + last_write_checksum: sha1:221678d588868630e1e79891ab988589bf83f9d2 + pristine_git_object: 8ee1a3bb595931e3dfd4329607b08dabee51d4f5 docs/models/researcheffort.md: id: 5b67676468b4 - last_write_checksum: sha1:8e3dc42805efa230a574cce116659bff6a550061 - pristine_git_object: e54e2072a76ab8bad9edc76563323a501a761204 + last_write_checksum: sha1:b5b71faf30e50d38cc54892aa94485ca8b434c42 + pristine_git_object: 27ca90da768f977245c5b117a6c9f4029ba73998 docs/models/researchinput.md: id: a5099da2ac37 last_write_checksum: sha1:a2212309beac7493be856f4dcff2c08af5fd3c9e pristine_git_object: 440a4440591c38081f66c1d9f37395b1b4faf89d + docs/models/researchinputunion.md: + id: 3e1e0afabaad + last_write_checksum: sha1:d1da5ad31f9be4625ba4d2403ade8c21278221ce + pristine_git_object: 182ea147f83ed413d2ac71d251c04ffc3da08762 docs/models/researchloc.md: id: 3aeace91cdd2 last_write_checksum: sha1:2666811f4486af8652e691adb62b0481f4fb2692 pristine_git_object: 54741e7b741e96ee1b130afa66523e61215aa21e docs/models/researchrequest.md: id: bf0af2378820 - last_write_checksum: sha1:92742ce12e177769db3e738aa27e9cbc7ccb986a - pristine_git_object: be8ad6ef6bd2a278d5e78fbab735437dcd3d6ae4 + last_write_checksum: sha1:1f7ee784cc77d10c887504c0516fa812100a3ef9 + pristine_git_object: d5f7ba39b64cab7d6ecb378e1f2879180846f561 docs/models/researchresponse.md: id: e2ac4b94e0e5 - last_write_checksum: sha1:0c004acb2376d5bd678f1daad46abf0fc5aeef68 - pristine_git_object: 58a1bc31d29cb4d036f52cbeb4bd2ffd95ce88ab + last_write_checksum: sha1:d0519db309441b605c339c6e3ec58c71b3857843 + pristine_git_object: e46094cc2c26f206fe0b884056ec8d1610c106dd + docs/models/researchresponse1.md: + id: a26d2da8ca99 + last_write_checksum: sha1:6a801fd4c56f21a37d9e9fa07a64cf6f02be6a93 + pristine_git_object: b62526204b9133b6902a07ff14278e6ea16c00fe + docs/models/researchtaskstreamevent.md: + id: 52269ee634b6 + last_write_checksum: sha1:18baa61d21e17def3df8255b116bd4eb74e5454b + pristine_git_object: f9fafb0b0cbd325a8818b02fb195e3d045c5f510 + docs/models/researchtaskstreameventdata.md: + id: 5cfbb580f307 + last_write_checksum: sha1:96d94adb5c63c4caff47f96bcab854d6a2ad2c33 + pristine_git_object: 56634dbec4ab3a31bdf8a8243e8c7b01f672a8b4 docs/models/researchtool.md: id: 68f310f2701f last_write_checksum: sha1:d5f85e978e3fb30fb80a8a362b0f8decf2a020fd @@ -297,50 +421,42 @@ trackedFiles: id: e315ded134b4 last_write_checksum: sha1:326a9f1e5a3413b94c6656df55c7fc580231c2b8 pristine_git_object: 5d468de3e2eddb033379b1d482fd8dd094bdb491 + docs/models/result.md: + id: b850437752c3 + last_write_checksum: sha1:e914b02f451373bbdfe8dc1c025377ad83625ce2 + pristine_git_object: bca20b4563a3a1669c36a11346e171b2934733a8 docs/models/results.md: id: c3a733d7c7d3 - last_write_checksum: sha1:cbc367f031a3d43e64e94d3ae44ee34abe109aec - pristine_git_object: c178f25b346e5be3f1f4811833c328077b7e1a38 + last_write_checksum: sha1:c66d85da84634b1a4cab5dcb60101f86a5ce90b9 + pristine_git_object: 342f0ea7f84a9f91517333400a3bd5b3a6df6593 docs/models/role.md: id: b694540a5b1e - last_write_checksum: sha1:74be0fbc7ed9966430da371eeadcdc51a4682568 - pristine_git_object: 3313880be54fd8c5d8761a4d8c8fb7c0e5b547c9 + last_write_checksum: sha1:d92cc3fcf9deaa2505304aa0b28a361993ba7df2 + pristine_git_object: 1f861cc27d1fd4b1b9a4020eb6dadcd0c53d2c57 docs/models/safesearch.md: id: 03e75d7f2e0d - last_write_checksum: sha1:6c928a770884f09c5ed23dc5932eadde3de93f0a - pristine_git_object: 2eae368bba6cff41ac768346fa6e665763443e8a - docs/models/searchcountry.md: - id: 4975534e7011 - last_write_checksum: sha1:99f418edaa8659ea654b30dfaf1d27da3a1e5853 - pristine_git_object: 9a95aec4988b5988130ec13393e9df3a3dd09330 + last_write_checksum: sha1:2c1d5ff1fa333a417ca8c1224163b53eb44b9a28 + pristine_git_object: 9efddcb711df36aa570dc30cbf40492b2b8f747f docs/models/searcheffort.md: id: d518e1b065c1 - last_write_checksum: sha1:daf57c9492c74819e1d4d51a62493000d7601ced - pristine_git_object: 95bb5b7394fb231b3b9cef813050863140ea65b9 - docs/models/searchfreshness.md: - id: 11d81669a994 - last_write_checksum: sha1:fa45060666c3fa42d6b65c2d7ec27b5cb0c9b483 - pristine_git_object: e9478183db391bc49b2f3a0a9a877e358cb4899d - docs/models/searchlivecrawl.md: - id: "953023757e55" - last_write_checksum: sha1:ffbcb9b63322706b9bc9fe1bae22fc6d83efc405 - pristine_git_object: ccf920074bc843e17549a6075b487b54b959e52a - docs/models/searchlivecrawlformats.md: - id: 3cc9911fcc52 - last_write_checksum: sha1:86e52959e8423e180e49d799a9eafa0cca09f1ec - pristine_git_object: 690de1f50a63ba69c5a73a5a2f7792b94b968436 + last_write_checksum: sha1:2f8259825f919cd9e5f4522d660f22547cd1c132 + pristine_git_object: 6f1c1ed8866231412ba7b38262ef17d92967ed09 + docs/models/searchmetadata.md: + id: 7fab78f275a5 + last_write_checksum: sha1:54940e0966c1feadafd666525700f988985b4e3c + pristine_git_object: 01105636a662e21e977fbaf3f04e189a0aafae5f docs/models/searchrequest.md: id: bc36c5e5aee1 - last_write_checksum: sha1:da9699efe63a7983e7265387fd12334508881ef3 - pristine_git_object: 1adb45e0d501c83f4ecdbb7b84879eefb20a3d52 + last_write_checksum: sha1:6c52337bf506fb50f2d130503a58a445860129ac + pristine_git_object: 3c07e4c98ceab5467d99acf5a1f5e05297d1f13a + docs/models/searchrequestbody.md: + id: d4e2dd80df2d + last_write_checksum: sha1:55c3281697e2230a70072aa02a55198f028642c7 + pristine_git_object: 82a042803a7a6e53f0c75573f97176d7edb67ce8 docs/models/searchresponse.md: id: d5606b4d403f - last_write_checksum: sha1:27a70c88fc494aa9dfda622d16da17d1a5c06996 - pristine_git_object: 5a85ee387b6c5acf7a1a032f16c0217f0b5eaffe - docs/models/searchsafesearch.md: - id: eb4c497e183a - last_write_checksum: sha1:ab9775fc51fb957ba3867ffbefc000e230c45d2c - pristine_git_object: fdad51ae07ea1ea8316d0a5d74d178751a5cc34a + last_write_checksum: sha1:bf8f309cc2f4d9a1236e1cc8afbfc3900861d2a3 + pristine_git_object: 0a0dcb77858a45a169e986cb0cd2b870db63acd7 docs/models/security.md: id: 452e4d4eb67a last_write_checksum: sha1:71fdd7bbeb4eccce70c9b87e9631d708571e0f17 @@ -349,26 +465,54 @@ trackedFiles: id: 6541ef7b41e7 last_write_checksum: sha1:483698db3a1c935665a55f062761e2250173f813 pristine_git_object: 174794d3a4732ea91a88d0b9b8d058127fc62f27 + docs/models/sourcecontrol.md: + id: 1da814573b6a + last_write_checksum: sha1:9158bddc57eaa41434172f0e0b3fc8f4ffc2f056 + pristine_git_object: cc42a853efacaf1a5168830b37f688661b367c5a + docs/models/streamresearchtaskrequest.md: + id: e573d311814e + last_write_checksum: sha1:174199f36f3c862ce04fe2fdb1b1abf401988b70 + pristine_git_object: 1b8c0b390c3b7d7f9aaa82a08239d206e57ec30e + docs/models/taskdetail.md: + id: 64a7dc3b1f6e + last_write_checksum: sha1:faac723566a124bf6c705c207be94532fdc0917e + pristine_git_object: a153ed2132976762294c766802429d01f5a07f80 + docs/models/taskdetailinput.md: + id: 0e67d3dc9f6f + last_write_checksum: sha1:43851a7102a6df448606d4987b17f3f55d232976 + pristine_git_object: 4470b82b1582d42bd5b3452edd88ea280d71f660 + docs/models/taskdetailstatus.md: + id: 84a875a28748 + last_write_checksum: sha1:8655f3ac475a3d120fa4dbb20b08034db4ddbd8e + pristine_git_object: 94533f5eda07dc9bf8e9a3186e39c474ef3c40dd + docs/models/taskresponse.md: + id: 19a599f24034 + last_write_checksum: sha1:3e3d82ef9d0cb6adfc441ce692b3be45ca8b23c6 + pristine_git_object: 759621ad139e006d0be3b6abcdf473fffe7ee680 + docs/models/taskresponsestatus.md: + id: 5c7a6b18f64f + last_write_checksum: sha1:38f0f176a41f4e286558144ab20279ba88e3a13e + pristine_git_object: b6ee96e4124e8515f75d49978b04889e16d6c3fb docs/models/tool.md: id: 8966139dbeed last_write_checksum: sha1:75d2ca9e2e2b19ceaa89db32c055b290709edc5a pristine_git_object: bb6d45112ef7ab37ea8cfab1f85eb32e9954679d docs/models/type.md: id: 98c32f09b2c8 - last_write_checksum: sha1:b600867b718b88b2f1e2cfe3c0f21b96d76810fe - pristine_git_object: db75a343248f6ae78c434dd3d6f08a7a5f6d5374 + last_write_checksum: sha1:d65a0a63069409574549340807ed0bbc2fc0be91 + pristine_git_object: 31b2152c9a0fe6cc1acfb95300c42c693e004a0f docs/models/utils/retryconfig.md: id: 4343ac43161c last_write_checksum: sha1:562c0f21e308ad10c27f85f75704c15592c6929d pristine_git_object: 69dd549ec7f5f885101d08dd502e25748183aebf docs/models/verbosity.md: id: a29d18f70f3d - last_write_checksum: sha1:9e4cb70588e5f8f031ba87a597cce3990bf352a5 - pristine_git_object: 3c30d679f2f512001915219f9b55774095c37ba6 - docs/models/web.md: - id: 79d8feb1f106 - last_write_checksum: sha1:d078cecf81002cee78b56ca4ebcbdfc301b80473 - pristine_git_object: b66b06507308267f48b10a0d012494f59d2d272d + last_write_checksum: sha1:bf8d880a60a36b0230d73c4883f8c6c945a5b5a8 + pristine_git_object: 35bfe62e1f58576bb086eb4566cca60afad89ffc + docs/models/webresult.md: + id: 8a203dd46ef9 + last_write_checksum: sha1:04c21830cc109bbce380a3193577a73b966c8212 + pristine_git_object: efac9dfaa8c5a97905db8f136a7ea043e3db865a docs/models/websearchtool.md: id: fc4df52fb9b5 last_write_checksum: sha1:3dd3b260252f363f9c6ae617fd38288bd007fad1 @@ -379,20 +523,20 @@ trackedFiles: pristine_git_object: b8a67d378796378511ff5969bc86f6c6406f3576 docs/sdks/contentssdk/README.md: id: 7ca17aeff32d - last_write_checksum: sha1:ce6d4513d733269954debb41f7f453ba8759723b - pristine_git_object: e4ce470a54ad136244116e5d218ef62f54402da9 + last_write_checksum: sha1:d9f338b11de3f55a458f91aeb72529ed44fbce79 + pristine_git_object: 3fc51bcb135deac43542ec0b6a01325768318776 docs/sdks/runs/README.md: id: 4598fd39b715 - last_write_checksum: sha1:a4fa67e2225b766de164057368ea6fea5e9035ed - pristine_git_object: c8d570f44294f1c6d2905701506867ef1ff4576b + last_write_checksum: sha1:42773cd1f5a922aad84df2549cdfd52a435460f4 + pristine_git_object: a653b912b254879a88ad52fcde0f68e9f077288e docs/sdks/search/README.md: id: 5c534716244c - last_write_checksum: sha1:2a9b67f8171c4ff82f2e46d8fef716e645fedfe8 - pristine_git_object: 72bfdc70ed5ced95fbf35318614a49976a823af1 + last_write_checksum: sha1:4fa9985c4cddc602d3f2508a7a0a4c6c07b7c299 + pristine_git_object: 7b1acd043305e94aeb58c3f68abb1875a4c4cb31 docs/sdks/you/README.md: id: 1abb954b0afb - last_write_checksum: sha1:4f5546910a5dfcda054541096cc98b945d41cab6 - pristine_git_object: 6be14705d30d36358fe5e422c8d84c45171b1467 + last_write_checksum: sha1:071449551e49c2ccbddc1b80f7c2e217972209a8 + pristine_git_object: 83c6f1cb4498f0ed2714f43cd7f6e5a34921e33e py.typed: id: 258c3ed47ae4 last_write_checksum: sha1:8efc425ffe830805ffcc0f3055871bdcdc542c60 @@ -403,8 +547,8 @@ trackedFiles: pristine_git_object: f456032107a9387ba6c98afd1c981df2f4b3d636 pyproject.toml: id: 5d07e7d72637 - last_write_checksum: sha1:52fa68a283dba10ff1069fed74cf81ab03fc1468 - pristine_git_object: 290ebddeeb9306329af4f48500a7fc9a708eff70 + last_write_checksum: sha1:59d016cef591ae38d2ac6fd79f3d1b77106ae0cb + pristine_git_object: 14a4f0420921775a54883a1739893d39c0390216 scripts/publish.sh: id: fe273b08f514 last_write_checksum: sha1:adc9b741c12ad1591ab4870eabe20f0d0a86cd1a @@ -423,28 +567,28 @@ trackedFiles: pristine_git_object: 48d0589d2a8c91e461bf078223e50797256c6125 src/youdotcom/_hooks/types.py: id: 0b21d2f9c319 - last_write_checksum: sha1:0f33c86fb76d38a27e731d37766a5a1a8467e29e - pristine_git_object: 13987cea395263dc4b60a5ee4cb9d8c65f95fc9f + last_write_checksum: sha1:2b04fb1ed665dcd77bb36a68db682daa2723bf86 + pristine_git_object: 2b03ad3cd42aa4614839ded8cdb336383c9b8713 src/youdotcom/_version.py: id: 5224f82ecc7b - last_write_checksum: sha1:01235a099c799feb9e36352e6c10fcfd6371552b - pristine_git_object: c998bfdeb29e4fee6ea9edb89caaec14cac7c449 + last_write_checksum: sha1:b917b8efe7c8ce83878da460127443e6bd7baecf + pristine_git_object: a0b39ae64f789506856d30f214e24f327908f7ab src/youdotcom/agents.py: id: 0ec0f4c4e0d0 last_write_checksum: sha1:4b58c15455f5410f050cfc8be831bfb6401d68ad pristine_git_object: 9090364d995f43bed128ecf046863c697691fd83 src/youdotcom/basesdk.py: id: c1c9ef882178 - last_write_checksum: sha1:21f701e27915aa678685dead53bb65f8b9c1f25b - pristine_git_object: ea145dd9a33a92ca4d64ca9ee88d499dcf8adc78 + last_write_checksum: sha1:403b2306c57c2fb84c5ee7048eaf168d573bdae4 + pristine_git_object: 8665d7f872dae803866e57bda2cc9288fb8d5fe0 src/youdotcom/contents_sdk.py: id: 0684c08251f6 - last_write_checksum: sha1:df4747161f79c38ccdcfbdf858f361e8d611ca11 - pristine_git_object: a21a07c01058262259ce70c61c5cc04d708e2086 + last_write_checksum: sha1:6e1927f6fa39fa8a866ddb909ba64aec9cdde2c8 + pristine_git_object: e7ce6138c8d3f5c9fb1be8963388917ae2c7b31d src/youdotcom/errors/__init__.py: id: e7ee44aa2c0f - last_write_checksum: sha1:3cda7690491c92a99b17243d42ffdc41cdb64abd - pristine_git_object: 2193daa39144685fdad94368aae2789a134d11d3 + last_write_checksum: sha1:37d94bfba5a24d0f1c77126e56f9fcecd537f7d2 + pristine_git_object: aea2e79bd23df3d7c47b0bc7a62a205ae5b6a023 src/youdotcom/errors/agentruns400response_error.py: id: 6e04ce5f87f1 last_write_checksum: sha1:3d8694955e3799f0c762f605074d403b8b2203ab @@ -461,22 +605,46 @@ trackedFiles: id: 92163cd72b73 last_write_checksum: sha1:0589479e94a35d68deac9064f098ce0569b3e36d pristine_git_object: 443fd304ff07e7623911e094281ab2f1661843f1 + src/youdotcom/errors/finance_researchop.py: + id: 83beaa919a2b + last_write_checksum: sha1:de34cbe163c5b9dad85b5bf88f2870e4d9956dd1 + pristine_git_object: a42f34dbe086f2af1e4fbfbd5a9184037646ca1f + src/youdotcom/errors/forbidden_response_error.py: + id: 777fa6546b44 + last_write_checksum: sha1:afe4203758b823712ebd8c8be02fdd7649e94c64 + pristine_git_object: 575dded2c86c4c8fd55107c2dda8a09a92de0908 + src/youdotcom/errors/getresearchtaskop.py: + id: 3bbd7b3b0589 + last_write_checksum: sha1:ba7c6a4647bd1b95ad1f3321c6f9e938a9a41f9e + pristine_git_object: 1c3cda1632e855320489e7e02b7c9c77ade802a8 + src/youdotcom/errors/internalservererror_response.py: + id: 7b3a4b21c280 + last_write_checksum: sha1:a82788befcb10990f3271d8b9b461956990cd78d + pristine_git_object: 4a8c1a8ff1c1b6906af38e19b492a98636ed46bb src/youdotcom/errors/no_response_error.py: id: 9f953dc697cf last_write_checksum: sha1:7f326424a7d5ae1bcd5c89a0d6b3dbda9138942f pristine_git_object: 1deab64bc43e1e65bf3c412d326a4032ce342366 src/youdotcom/errors/researchop.py: id: 8143ca635d3f - last_write_checksum: sha1:66e92b01868dda50157ced7254e072293e868283 - pristine_git_object: b1a5836fc9ac80feae48c5bc937f5ebb61dc803c + last_write_checksum: sha1:696a48d30e1e40cd8fc2ad162524107f0ab1dc2c + pristine_git_object: a64bebbb06286b54a281d95d5573ccf6c27dfb91 src/youdotcom/errors/responsevalidationerror.py: id: 0ad5034298b3 last_write_checksum: sha1:f95060059297e22c183f9e387df44eb03aac1f5c pristine_git_object: 8e3bb217198ec204c2c92aa0c3f1aa92ce1ec5c3 - src/youdotcom/errors/searchop.py: - id: d7ef659447d5 - last_write_checksum: sha1:84a1f680cc4df4ccb54b5c7aaa7c68c02a431d67 - pristine_git_object: 9bb37947302e02e38676bbef8f5f12b4f831c3df + src/youdotcom/errors/streamresearchtaskop.py: + id: 83ae010b3434 + last_write_checksum: sha1:5056e9711ebfd40b10dc74483c52051695f7c14c + pristine_git_object: a2ee4a014f64397314764561c58aecc0b9ff37a8 + src/youdotcom/errors/unauthorized_response_error.py: + id: 7f23fe11fee3 + last_write_checksum: sha1:8fffa1bbbae4188098c085f1fd8e2374abb47a78 + pristine_git_object: dbc2f9f28572fbbb2dbbaad3c9cd2c387bd969a4 + src/youdotcom/errors/unprocessableentity_response_error.py: + id: d4a6fd9c273f + last_write_checksum: sha1:5a25ee76ff251002f745a4fd23eb0735e11903dd + pristine_git_object: f9153650c749efc975429515a53eb321b2e42225 src/youdotcom/errors/youdefaulterror.py: id: 4a5d0619a409 last_write_checksum: sha1:1b4ccd64f7c7845f589d1a43735da0a0ea186459 @@ -491,92 +659,124 @@ trackedFiles: pristine_git_object: 89560b566073785535643e694c112bedbd3db13d src/youdotcom/models/__init__.py: id: ad350e4fd8c1 - last_write_checksum: sha1:b7ac540f79ae19ad6bc96101a99b3d2df3d29325 - pristine_git_object: 804e5c280c7b35c49fcb7ea9db759c0b5444274d + last_write_checksum: sha1:7b94b19f0f1c279723fdc80285e0826e19950a0a + pristine_git_object: 8758fa14ef9e0288c17ba7f9801c269c5245a4b9 src/youdotcom/models/advancedagentrunsrequest.py: id: 6bb8d5dd67d4 - last_write_checksum: sha1:d1207266cea794cac55224e3bc6aed2f66422949 - pristine_git_object: f271b2bff38f6c55a2a6e3f890ef04bb6b1405c9 + last_write_checksum: sha1:ac336ef380378a65f90a9b70dba67b2e2db26401 + pristine_git_object: 8511c0aebc0698e8aad0bee3b9766271eb65f447 src/youdotcom/models/agentruns422response_error.py: id: 6731cdd29afd last_write_checksum: sha1:e9c70fe90257d4f3a93ea7d730e4d7f662dbd7bb pristine_git_object: 41240562660e2a4dd1ee4823ae31e843ce51c056 src/youdotcom/models/agentrunsbatchresponse.py: id: 38fe9f202ccb - last_write_checksum: sha1:0717ce0715c0cd84de6f21e9822c8029197bcf67 - pristine_git_object: 187616cbe44da1ecaeffe76bb0fc55985f1e242d + last_write_checksum: sha1:1c6ec4448a824d3dedeeabea43fd14df00bf7cf1 + pristine_git_object: 6de8970eacc2f5390a275f564c7cce17f304df05 src/youdotcom/models/agentrunsresponseoutput.py: id: 6ac5478f43e0 - last_write_checksum: sha1:8ebe3e761e97a1fcb81a3e180dd9643f705e7367 - pristine_git_object: 14726f52263a8f553a12ee6dd24f5451a65adf62 + last_write_checksum: sha1:bb2f18b91a61766c5c050214e98999e8cc0f111d + pristine_git_object: 1eaa631665b302a4aea097bf859bc07727e2c335 src/youdotcom/models/agentrunsresponsewebsearchresult.py: id: e85bdd982508 - last_write_checksum: sha1:03d003761e73fea392805382b36545792acac7dd - pristine_git_object: a0a105cab1cf1c161370a4579b7ef93cda0ed4c0 + last_write_checksum: sha1:dfe451543399d856efea36949e16d0cebdef9299 + pristine_git_object: 7f9d3c3ae17aecd76f7f5c4f408979ed8f5b2feb src/youdotcom/models/agentrunsstreamingresponse.py: id: 1423c3f03bf9 last_write_checksum: sha1:0fff2d6e0785c7744a50455853c309f7ca609fd5 pristine_git_object: 1a2c119e6ecee3f1116cc98968a9cc86cdde503e src/youdotcom/models/agentsrunsop.py: id: b0d55d74eb29 - last_write_checksum: sha1:3b2198586c6819837835eb8a6a2b5e90722f0136 - pristine_git_object: 2ac90c01209da9babf694e453a9bdd7ba8f7f517 + last_write_checksum: sha1:7b2b7d6c14bb77b25efc2ddc29e9336d04aed473 + pristine_git_object: 52a57d5de789822fbeb65ad45b7273c6402ea1d2 src/youdotcom/models/computetool.py: id: 5353a2de6f97 last_write_checksum: sha1:4c65a3868a85ac0ca4a08e8ab10f1dc34bf7f364 pristine_git_object: 12538817aaf1efe50c87ee29995f423b1c89567c src/youdotcom/models/contents.py: id: c1d5af212a4c - last_write_checksum: sha1:c3b6a119c617166269004dd2967c5121c36ad98d - pristine_git_object: ecf165e3b60ee5779f96276b3ce6ae9a59a0c381 + last_write_checksum: sha1:b7a585d24a3635704779bafc6141f811268cd499 + pristine_git_object: 37d69b8fd439a008b0a9459819fc35135b03f572 src/youdotcom/models/contentsformats.py: id: 0d5f457da03c last_write_checksum: sha1:c226d046ba051044bde0ba070143fcc2cabe5ed8 pristine_git_object: 91f16e0e9d599e1a9c4b819ac25ac07a8246cb4c src/youdotcom/models/contentsmetadata.py: id: 4e49905aae0b - last_write_checksum: sha1:065212c12a231f1f080a194d6b31375a5fb2ff88 - pristine_git_object: 6324e63c7f280e72fd1c0d1711b78b3c37be448b + last_write_checksum: sha1:5dc844d2c2bf653caff438b7f4ee1eadf37e0ced + pristine_git_object: b6b8e50f233dedf42297e1f52cce6b2430ffb833 src/youdotcom/models/contentsop.py: id: ca42ef875c5f - last_write_checksum: sha1:281db131fb9b8e4feef7f8c31cea58fc99aa89e0 - pristine_git_object: fc47c7fd77d691d12d06e8cd6a2f9d8ddca8a7a3 + last_write_checksum: sha1:28dee18e3e275608363bf81b38210df548a3a958 + pristine_git_object: 9e102924a02d1165ca1b3849f88657a9032065be src/youdotcom/models/country.py: id: 725d2a57cc07 last_write_checksum: sha1:7827b3e65afd2ee86078e45ac373cedfe5d68766 pristine_git_object: 720e60691d5f653cae80488c00e54063953596e8 src/youdotcom/models/customagentrunsrequest.py: id: 089bb3a5b607 - last_write_checksum: sha1:d29b92460db8c7195464d851d436d18c47334357 - pristine_git_object: 759f45bd1053d6b4f52400434edadb027661958b + last_write_checksum: sha1:745881c56a61fc877f3a5f28b6a69faf5663a945 + pristine_git_object: 181e560c7a144fe28f84bbcc3d35d8ba67d5ce0e src/youdotcom/models/expressagentrunsrequest.py: id: 0f698f43a90d - last_write_checksum: sha1:182e79b7735aaa20c2e7203a787267cde8f2c2b7 - pristine_git_object: f4f5de0e9c96adf70854876f0eb06d319e464eba + last_write_checksum: sha1:44ac13388d585adceb28c1164d852ddb15550327 + pristine_git_object: 8211741b2403722bb0dff06a7b31b29918dd544d + src/youdotcom/models/finance_researchop.py: + id: 581b6926206d + last_write_checksum: sha1:add5e681c935ccee9eb22808c85d7dd9eb4e28af + pristine_git_object: 1d886fde93e4f319ebc62167440ca558bef75a18 + src/youdotcom/models/financeresearcheffort.py: + id: 493d242448a3 + last_write_checksum: sha1:a4e4ea1e984849bc3ea3eb52697387e1320aa2ac + pristine_git_object: 417d8052d4135f81767efc9e9349f71cd2b3c4d8 src/youdotcom/models/freshness.py: id: c7c960c20e5e - last_write_checksum: sha1:48ae86226706f17277fc58f132b55eef545888a4 - pristine_git_object: da5bd9fe7c0e3abd649a48b5114ce9845646d3bc + last_write_checksum: sha1:9ed62b1edeac55bd86aa69d3092a6864046487ff + pristine_git_object: 83281eb315fc1d2675302c34c15f81d408fd462f + src/youdotcom/models/freshnessvalue.py: + id: 53cb2e2fc925 + last_write_checksum: sha1:1408b2d4e83ed5cf93f15da95797b6c44ea00b2a + pristine_git_object: 66ff7788852bd6ff23f2d4941e2fffb436740b30 + src/youdotcom/models/getresearchtaskop.py: + id: 54f00a55a577 + last_write_checksum: sha1:901338dba6e5b7e104bb5b98de6655d8671a3d71 + pristine_git_object: 0330943d8885190cba464af0de5b3b6d900e3833 src/youdotcom/models/language.py: id: 4e51e1ee857d - last_write_checksum: sha1:ad95c07ec07475141310e8ae5b4ac02dc1fbd703 - pristine_git_object: 25de23324976b84667fff8470e76d190d0e0f98a + last_write_checksum: sha1:6fb833b3b60ce6c233218377a550303b3daece8a + pristine_git_object: 83704f11b3829b3776d6551d059209d9c6c76858 src/youdotcom/models/livecrawl.py: id: e5dd4948ff3f last_write_checksum: sha1:f2b60aa9d84b622f961f81f781574f958c247953 pristine_git_object: c28464e9c8784e91c7f1e8e2ea417220040eaf58 src/youdotcom/models/livecrawlformats.py: id: 775d02437b81 - last_write_checksum: sha1:9dcad24e8794aefcb929db54d21594ef9809cbb8 - pristine_git_object: 27c9da850bdb854fab602fa2f7df88aedea33b40 + last_write_checksum: sha1:09894aa858c80f89c322d61c378ee033f3e9aef1 + pristine_git_object: ceca02dde497a61488a00d1312ece701f6f7ce37 + src/youdotcom/models/newsresult.py: + id: daffe8db4b1b + last_write_checksum: sha1:d8a48605207bf6cbd4a143c3d5acd5bdf492f89e + pristine_git_object: 9a892957291c73bfde00e33cdfd3c19e4428035f src/youdotcom/models/reportverbosity.py: id: 5a8683f42b91 last_write_checksum: sha1:b7c084407a5584d770deb61970d4953825a3bd2c pristine_git_object: 14a8b432a575bc4c291723e98f2586beae4f5939 + src/youdotcom/models/researcheffort.py: + id: 24a4c58a1aa5 + last_write_checksum: sha1:8bd93d9e73d30e730759c5a8fcecd16282563268 + pristine_git_object: 2384195f5785456082cbb4cf279bbcce118ff0e1 src/youdotcom/models/researchop.py: id: c1ae2c3f13d9 - last_write_checksum: sha1:fc7c95702b93a5e31edbda349b8acc0199ad084b - pristine_git_object: f8ec45ce17672dc92b004d989d6c677f89b06668 + last_write_checksum: sha1:624384e2b2c824e19f0f258811dcfcdb0b97365b + pristine_git_object: d4fbf574537b093ea5b8a2709ea30141e5ae04e9 + src/youdotcom/models/researchresponse.py: + id: 4d52580e1e41 + last_write_checksum: sha1:9314abaf991725916e873934a5b513cb4d103b14 + pristine_git_object: 01ec38768ccfed561f4f42f6dc60e3e66d97c45f + src/youdotcom/models/researchtaskstreamevent.py: + id: 3a6d8a77978d + last_write_checksum: sha1:e46ae161e08b95c5c3569810d2f181c80eaca919 + pristine_git_object: 0198893c2995d4795964018ca273d439e8dddf6a src/youdotcom/models/researchtool.py: id: 4e0236b1b670 last_write_checksum: sha1:e90b83ae9dbf18da27bbbf2db81fbd18a16cb4d1 @@ -617,18 +817,50 @@ trackedFiles: id: 8864bba8ca75 last_write_checksum: sha1:8709eb40bec8635ce221ddfad0a3abc8ee3b400d pristine_git_object: cec092e4f820d3dfd8d09f8b7bc224c418b8fd49 + src/youdotcom/models/searchmetadata.py: + id: de041a4286e9 + last_write_checksum: sha1:1adb9cdd44100896aa387cd1f995cb86994dc34f + pristine_git_object: 758b0c2297d21b7c483fd164f3778dc9506e5718 src/youdotcom/models/searchop.py: id: 525c0a4e8872 - last_write_checksum: sha1:ea521b1b3cf6d34aeabe0268b571f4eec698bf23 - pristine_git_object: a06a0fdcef28814a51726bba187cbc82958d7f48 + last_write_checksum: sha1:c17146d3f6f1ff1a75a1750c9050ffc53e4f301a + pristine_git_object: 358a4fa5770c26927df2b1876822b10580a881e7 + src/youdotcom/models/searchpostop.py: + id: 57d9a686a4d2 + last_write_checksum: sha1:393291e98f851ad723e1b239a69c0a045c38fc67 + pristine_git_object: e0f8b5b537deccd0bd4213f783dd502cf340597d + src/youdotcom/models/searchrequestbody.py: + id: e36cf1876cdd + last_write_checksum: sha1:3a6820ed3a4fa6d14011f0def1043a0524ff5ae1 + pristine_git_object: df94c4c545951a34a0eb8b58a8139593395ee546 + src/youdotcom/models/searchresponse.py: + id: 777112b1d670 + last_write_checksum: sha1:8c597f06af6b2f1d47ae564d1a462b6dadf636ac + pristine_git_object: ee9313ea8ad7ca83082a1c7e2b726ef7fcef750f src/youdotcom/models/security.py: id: 3a94d17768c4 - last_write_checksum: sha1:8b1c11f19ca3684c0330a4884c0f45b165578a7b - pristine_git_object: 77789954602ab51b6083ecc41e63511cf49d9e5a + last_write_checksum: sha1:f55a74714c74d2f19f22b23fd8c798be81828804 + pristine_git_object: 2313b5207ca434956401f8f32265c71790396370 + src/youdotcom/models/streamresearchtaskop.py: + id: 9ee96698de20 + last_write_checksum: sha1:c629028be89a6fe747f522ff374503734b7c2082 + pristine_git_object: 6b57b1d712ba0cdf5210351e21ab5527485b10be + src/youdotcom/models/taskdetail.py: + id: b6be40ca0f7b + last_write_checksum: sha1:1900f846e29ea42834731f6ea1873a5f44ea0387 + pristine_git_object: 568ca9f37138856cfb07a88bcb9eac2442c7376c + src/youdotcom/models/taskresponse.py: + id: 6a323de664ed + last_write_checksum: sha1:1731ce101d9742dbbbeb6d42df5550eef9fb40b0 + pristine_git_object: 490476df671a9efc330612dec7612d56822c0713 src/youdotcom/models/verbosity.py: id: 68768141a514 last_write_checksum: sha1:ba9d4ee35bed37f14d67ca27f38cb0efc44eb132 pristine_git_object: 666d75aaa5f5ab22a259d8fe1aa08f599c79e104 + src/youdotcom/models/webresult.py: + id: 4f4f8e119dbe + last_write_checksum: sha1:160bda43c198787655edcc6b031824488929b183 + pristine_git_object: d8f6f6c49923e82a46eb6d077d309f20d2eb0c96 src/youdotcom/models/websearchtool.py: id: 8240ffdc3807 last_write_checksum: sha1:44338571c786fbf9377a82ca0e0ff57a26472e66 @@ -639,40 +871,44 @@ trackedFiles: pristine_git_object: 3e38f1a929f7d6b1d6de74604aa87e3d8f010544 src/youdotcom/runs.py: id: 8011c1ffa5a1 - last_write_checksum: sha1:08b88bce4f60cfac1d6744f077e27175a20b1987 - pristine_git_object: 0fc591713191bcd659057fcdc05e2b07139b3457 + last_write_checksum: sha1:ceffbd7959577edb2d8f3b23d565567fbb008e97 + pristine_git_object: 0c63569f3630825cea41a82c0429fda5496b41ef src/youdotcom/sdk.py: id: 90954e74e7b7 - last_write_checksum: sha1:5e14bed0ce6e55976b40bacba13ccd526ec818fe - pristine_git_object: c72bef1f7ba827516449fd4e30e1378189a9542b + last_write_checksum: sha1:270f5ea1daaf823dc0fdd50b47585b244c9bc29d + pristine_git_object: 67696cc8ec71360da47db5ee5fd49ec8a8c9f4b5 src/youdotcom/sdkconfiguration.py: id: eb56427350d9 last_write_checksum: sha1:f62ffed1bbe732f18d96076c622fccf3eb8c2f6c pristine_git_object: ea49a89ef4840777202f1440fbf116e2f63d7a56 src/youdotcom/search.py: id: 5e475f9db47f - last_write_checksum: sha1:fa2b85d71f14cc589fbb190c2475f387aea8f5fa - pristine_git_object: 355ce7774b9722b1f02ab98634f9481831ace609 + last_write_checksum: sha1:9176b010d442d11586bc42b493974e297984509d + pristine_git_object: 9e69fa0416a9ed0e8583977fde7d1901dfdd13ba src/youdotcom/types/__init__.py: id: 5e0774b59bbc - last_write_checksum: sha1:140ebdd01a46f92ffc710c52c958c4eba3cf68ed - pristine_git_object: fc76fe0c5505e29859b5d2bb707d48fd27661b8c + last_write_checksum: sha1:f9ad14217f832e74f594285960125add50324be9 + pristine_git_object: faa268137bc01c9d08cfadc4797017db48747a96 + src/youdotcom/types/base64fileinput.py: + id: 7a5db9c7c0dd + last_write_checksum: sha1:1522687ae3398374c35710cad993a6e82b5ab99d + pristine_git_object: 862566fe2b1db830276b390e136e65090e5963d2 src/youdotcom/types/basemodel.py: id: cbf130c761e5 last_write_checksum: sha1:10d84aedeb9d35edfdadf2c3020caa1d24d8b584 pristine_git_object: a9a640a1a7048736383f96c67c6290c86bf536ee src/youdotcom/utils/__init__.py: id: bd4acfd8ce6a - last_write_checksum: sha1:0f93d821f9cb3e061ea125d881bb6f61166738dd - pristine_git_object: aded7597c2bbc4e3fa7f555755d894671ac57741 + last_write_checksum: sha1:527db7a6c93948ecdec6a33f56b6551ae44f0180 + pristine_git_object: c48a36ca3d6e47e384b133bc0bcb482c6d8589b9 src/youdotcom/utils/annotations.py: id: bc5c21b7250e last_write_checksum: sha1:a4824ad65f730303e4e1e3ec1febf87b4eb46dbc pristine_git_object: 12e0aa4f1151bb52474cc02e88397329b90703f6 src/youdotcom/utils/datetimes.py: id: 2b6c0dd73070 - last_write_checksum: sha1:c721e4123000e7dc61ec52b28a739439d9e17341 - pristine_git_object: a6c52cd61bbe2d459046c940ce5e8c469f2f0664 + last_write_checksum: sha1:fa47d54c549ff9b95496ce91930fadfb04a36be9 + pristine_git_object: adad24762cf452d137abe94fda0fd26224cef593 src/youdotcom/utils/dynamic_imports.py: id: 09b830ea97a1 last_write_checksum: sha1:a1940c63feb8eddfd8026de53384baf5056d5dcc @@ -683,12 +919,12 @@ trackedFiles: pristine_git_object: 3324e1bc2668c54c4d5f5a1a845675319757a828 src/youdotcom/utils/eventstreaming.py: id: 5f31d9da5a93 - last_write_checksum: sha1:ffa870a25a7e4e2015bfd7a467ccd3aa1de97f0e - pristine_git_object: f2052fc22d9fd6c663ba3dce019fe234ca37108b + last_write_checksum: sha1:7d1dc68f8b48486ab646653aa05cc38752e1f912 + pristine_git_object: a8d4fe5cc88d3c7337339e1b36a61bbf7ca8c4eb src/youdotcom/utils/forms.py: id: 1bf9b877c054 - last_write_checksum: sha1:0ca31459b99f761fcc6d0557a0a38daac4ad50f4 - pristine_git_object: 1e550bd5c2c35d977ddc10f49d77c23cb12c158d + last_write_checksum: sha1:dcf527960992b8baef3b21937c7e6c4e652630c0 + pristine_git_object: 193f264960f3014c81d41543b9023c14c44c12e6 src/youdotcom/utils/headers.py: id: f1171bccb6d8 last_write_checksum: sha1:7c6df233ee006332b566a8afa9ce9a245941d935 @@ -699,28 +935,28 @@ trackedFiles: pristine_git_object: 6ae3abd220a08af699d685aa61dd4168df6dbcfa src/youdotcom/utils/metadata.py: id: f6d2fb72eae3 - last_write_checksum: sha1:c6a560bd0c63ab158582f34dadb69433ea73b3d4 - pristine_git_object: 173b3e5ce658675c2f504222a56b3daaaa68107d + last_write_checksum: sha1:e703e5cbb5255144aacf86898d1420529afaaff8 + pristine_git_object: 5abddd588837ac297050ca3b543627faadb350a9 src/youdotcom/utils/queryparams.py: id: 1340b8e3e103 last_write_checksum: sha1:b94c3f314fd3da0d1d215afc2731f48748e2aa59 pristine_git_object: c04e0db82b68eca041f2cb2614d748fbac80fd41 src/youdotcom/utils/requestbodies.py: id: 1276b4c43669 - last_write_checksum: sha1:41e2d2d2d3ecc394c8122ca4d4b85e1c3e03f054 - pristine_git_object: 1de32b6d26f46590232f398fdba6ce0072f1659c + last_write_checksum: sha1:e1fef575283b7fe7fe2ad392dbbb3fb105309124 + pristine_git_object: 591415af8e64baa410627b507d2740afb5387d13 src/youdotcom/utils/retries.py: id: 384c61cdc8f6 - last_write_checksum: sha1:471372f5c5d1dd5583239c9cf3c75f1b636e5d87 - pristine_git_object: af07d4e941007af4213c5ec9047ef8a2fca04e5e + last_write_checksum: sha1:3585b891142f30a597fbf7a2f0340700babef8e4 + pristine_git_object: ca7b59efebbbd9545744d0207ef42725c4cc5143 src/youdotcom/utils/security.py: id: 41e3b3176b50 - last_write_checksum: sha1:8f41e203536f0ab841ea31498ca73556d5cb92b1 - pristine_git_object: e51915d1bd67d63cb62077f19b1b5cd13f747dd6 + last_write_checksum: sha1:802ca60459e7a12b60dba8c2060dcdedd52fd1ee + pristine_git_object: cd67559004f44eaeec27d09183a6b7ce2fced666 src/youdotcom/utils/serializers.py: id: 360f8e2583cc - last_write_checksum: sha1:ce1d8d7f500a9ccba0aeca5057cee9c271f4dfd7 - pristine_git_object: 14321eb479de81d0d9580ec8291e0ff91bf29e57 + last_write_checksum: sha1:7485f1425b0661fd84836186570df90207eec6af + pristine_git_object: 1031ed930bad5ece220cf7416a56c29f40f0588b src/youdotcom/utils/unmarshal_json_response.py: id: ae5a7f2d9dc3 last_write_checksum: sha1:aae3840de6b5894dcf8f167fd83b6b77294041ef @@ -826,6 +1062,10 @@ examples: query: "Your query" language: "EN" count: 10 + include_domains: "nytimes.com,bbc.com" + exclude_domains: "spam-site.com,other-site.com" + boost_domains: "nytimes.com,wired.com" + crawl_timeout: 10 responses: "200": application/json: {"results": {"web": [{"url": "https://you.com", "title": "The World's Greatest Search Engine!", "description": "Search on YDC", "snippets": ["I'm an AI assistant that helps you get more done. What can I help you with?"], "thumbnail_url": "https://www.somethumbnailsite.com/thumbnail.jpg", "page_age": "2025-06-25T11:41:00", "authors": ["John Doe"], "favicon_url": "https://someurl.com/favicon"}], "news": [{"title": "Exclusive | You.com becomes the backbone of the EU's AI strategy", "description": "As the EU's AI strategy is being debated, You.com becomes the backbone of the EU's AI strategy.", "page_age": "2025-06-25T11:41:00", "thumbnail_url": "https://www.somethumbnailsite.com/thumbnail.jpg", "url": "https://www.you.com/news/eu-ai-strategy-youcom"}]}, "metadata": {"search_uuid": "942ccbdd-7705-4d9c-9d37-4ef386658e90", "query": "Your query", "latency": 0.123}} @@ -835,12 +1075,18 @@ examples: application/json: {} "500": application/json: {} + "422": + application/json: {} missingApiKey: parameters: query: query: "Your query" language: "EN" count: 10 + include_domains: "nytimes.com,bbc.com" + exclude_domains: "spam-site.com,other-site.com" + boost_domains: "nytimes.com,wired.com" + crawl_timeout: 10 responses: "401": application/json: {"detail": "API key is required"} @@ -850,6 +1096,10 @@ examples: query: "Your query" language: "EN" count: 10 + include_domains: "nytimes.com,bbc.com" + exclude_domains: "spam-site.com,other-site.com" + boost_domains: "nytimes.com,wired.com" + crawl_timeout: 10 responses: "401": application/json: {"detail": "Invalid or expired API key"} @@ -859,6 +1109,10 @@ examples: query: "Your query" language: "EN" count: 10 + include_domains: "nytimes.com,bbc.com" + exclude_domains: "spam-site.com,other-site.com" + boost_domains: "nytimes.com,wired.com" + crawl_timeout: 10 responses: "401": application/json: {"detail": ""} @@ -868,6 +1122,10 @@ examples: query: "Your query" language: "EN" count: 10 + include_domains: "nytimes.com,bbc.com" + exclude_domains: "spam-site.com,other-site.com" + boost_domains: "nytimes.com,wired.com" + crawl_timeout: 10 responses: "403": application/json: {"detail": "Missing required scopes"} @@ -877,6 +1135,10 @@ examples: query: "Your query" language: "EN" count: 10 + include_domains: "nytimes.com,bbc.com" + exclude_domains: "spam-site.com,other-site.com" + boost_domains: "nytimes.com,wired.com" + crawl_timeout: 10 responses: "500": application/json: {"detail": "Internal authentication error"} @@ -886,49 +1148,66 @@ examples: query: "Your query" language: "EN" count: 10 + include_domains: "nytimes.com,bbc.com" + exclude_domains: "spam-site.com,other-site.com" + boost_domains: "nytimes.com,wired.com" + crawl_timeout: 10 responses: "500": application/json: {"detail": "Internal authorization error"} + invalidParams: + parameters: + query: + query: "What are the latest geopolitical updates from India" + count: 10 + language: "EN" + include_domains: "nytimes.com,bbc.com" + exclude_domains: "spam-site.com,other-site.com" + boost_domains: "nytimes.com,wired.com" + crawl_timeout: 10 + responses: + "422": + application/json: {"error": "invalid request parameter(s)"} contents: missingApiKey: requestBody: - application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10} + application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10, "max_age": 86400} responses: "401": application/json: {"detail": "API key is required"} invalidOrExpired: requestBody: - application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10} + application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10, "max_age": 86400} responses: "401": application/json: {"detail": "Invalid or expired API key"} otherAuthParsing: requestBody: - application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10} + application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10, "max_age": 86400} responses: "401": application/json: {"detail": ""} missingScopes: requestBody: - application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10} + application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10, "max_age": 86400} responses: "403": application/json: {"detail": "Missing required scopes"} authFailure: requestBody: - application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10} + application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10, "max_age": 86400} responses: "500": application/json: {"detail": "Internal authentication error"} authorizationFailure: requestBody: - application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10} + application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10, "max_age": 86400} responses: "500": application/json: {"detail": "Internal authorization error"} speakeasy-default-contents: requestBody: - application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10} + application/json: {"urls": ["https://www.you.com"], "formats": ["html", "markdown"], "crawl_timeout": 10, "max_age": 86400} responses: "200": application/json: [{"url": "https://www.you.com", "title": "The best website in the world", "metadata": {"site_name": "You.com", "favicon_url": "https://api.ydc-index.io/favicon?domain=you.com&size=128"}}] @@ -941,7 +1220,7 @@ examples: research: speakeasy-default-research: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "", "research_effort": "standard", "background": false} responses: "200": application/json: {"output": {"content": "", "content_type": "text", "sources": []}} @@ -955,61 +1234,225 @@ examples: application/json: {} missingApiKey: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "", "research_effort": "standard", "background": false} responses: "401": application/json: {"detail": "API key is required"} invalidOrExpired: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "", "research_effort": "standard", "background": false} responses: "401": application/json: {"detail": "Invalid or expired API key"} otherAuthParsing: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "", "research_effort": "standard", "background": false} responses: "401": application/json: {"detail": ""} missingScopes: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "", "research_effort": "standard", "background": false} responses: "403": application/json: {"detail": "Missing required scopes"} missingField: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "", "research_effort": "standard", "background": false} responses: "422": application/json: {"detail": [{"type": "missing", "loc": ["body", "input"], "msg": "Field required", "input": {}}]} invalidEnum: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "", "research_effort": "standard", "background": false} responses: "422": application/json: {"detail": [{"type": "enum", "loc": ["body", "research_effort"], "msg": "Input should be 'lite', 'standard', 'deep' or 'exhaustive'", "input": "invalid_value", "ctx": {"expected": "'lite', 'standard', 'deep' or 'exhaustive'"}}]} stringTooLong: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "", "research_effort": "standard", "background": false} + responses: + "422": + application/json: {"detail": [{"type": "string_too_long", "loc": ["body", "input"], "msg": "String should have at most 40000 characters", "input": "", "ctx": {"max_length": 40000}}]} + invalidJson: + requestBody: + application/json: {"input": "", "research_effort": "standard", "background": false} + responses: + "422": + application/json: {"detail": [{"type": "json_invalid", "loc": ["body", 115], "msg": "JSON decode error", "input": {}, "ctx": {"error": "Invalid control character at"}}]} + authFailure: + requestBody: + application/json: {"input": "", "research_effort": "standard", "background": false} + responses: + "500": + application/json: {"detail": "Internal authentication error"} + authorizationFailure: + requestBody: + application/json: {"input": "", "research_effort": "standard", "background": false} + responses: + "500": + application/json: {"detail": "Internal authorization error"} + searchPost: + missingApiKey: + requestBody: + application/json: {"query": "What are the latest geopolitical updates from India", "count": 10, "language": "EN", "include_domains": ["nytimes.com", "bbc.com"], "exclude_domains": ["spam-site.com", "other-site.com"], "boost_domains": ["nytimes.com", "wired.com"], "crawl_timeout": 10} + responses: + "401": + application/json: {"detail": "API key is required"} + invalidOrExpired: + requestBody: + application/json: {"query": "What are the latest geopolitical updates from India", "count": 10, "language": "EN", "include_domains": ["nytimes.com", "bbc.com"], "exclude_domains": ["spam-site.com", "other-site.com"], "boost_domains": ["nytimes.com", "wired.com"], "crawl_timeout": 10} + responses: + "401": + application/json: {"detail": "Invalid or expired API key"} + otherAuthParsing: + requestBody: + application/json: {"query": "What are the latest geopolitical updates from India", "count": 10, "language": "EN", "include_domains": ["nytimes.com", "bbc.com"], "exclude_domains": ["spam-site.com", "other-site.com"], "boost_domains": ["nytimes.com", "wired.com"], "crawl_timeout": 10} + responses: + "401": + application/json: {"detail": ""} + missingScopes: + requestBody: + application/json: {"query": "What are the latest geopolitical updates from India", "count": 10, "language": "EN", "include_domains": ["nytimes.com", "bbc.com"], "exclude_domains": ["spam-site.com", "other-site.com"], "boost_domains": ["nytimes.com", "wired.com"], "crawl_timeout": 10} + responses: + "403": + application/json: {"detail": "Missing required scopes"} + invalidParams: + requestBody: + application/json: {"query": "What are the latest geopolitical updates from India", "count": 10, "language": "EN", "include_domains": ["nytimes.com", "bbc.com"], "exclude_domains": ["spam-site.com", "other-site.com"], "boost_domains": ["nytimes.com", "wired.com"], "crawl_timeout": 10} + responses: + "422": + application/json: {"error": "invalid request parameter(s)"} + authFailure: + requestBody: + application/json: {"query": "What are the latest geopolitical updates from India", "count": 10, "language": "EN", "include_domains": ["nytimes.com", "bbc.com"], "exclude_domains": ["spam-site.com", "other-site.com"], "boost_domains": ["nytimes.com", "wired.com"], "crawl_timeout": 10} + responses: + "500": + application/json: {"detail": "Internal authentication error"} + authorizationFailure: + requestBody: + application/json: {"query": "What are the latest geopolitical updates from India", "count": 10, "language": "EN", "include_domains": ["nytimes.com", "bbc.com"], "exclude_domains": ["spam-site.com", "other-site.com"], "boost_domains": ["nytimes.com", "wired.com"], "crawl_timeout": 10} + responses: + "500": + application/json: {"detail": "Internal authorization error"} + speakeasy-default-search-post: + requestBody: + application/json: {"query": "What are the latest geopolitical updates from India", "count": 10, "language": "EN", "include_domains": ["nytimes.com", "bbc.com"], "exclude_domains": ["spam-site.com", "other-site.com"], "boost_domains": ["nytimes.com", "wired.com"], "crawl_timeout": 10} + responses: + "200": + application/json: {"results": {"web": [{"url": "https://you.com", "title": "The World's Greatest Search Engine!", "description": "Search on YDC", "snippets": ["I'm an AI assistant that helps you get more done. What can I help you with?"], "thumbnail_url": "https://www.somethumbnailsite.com/thumbnail.jpg", "page_age": "2025-06-25T11:41:00", "authors": ["John Doe"], "favicon_url": "https://someurl.com/favicon"}], "news": [{"title": "Exclusive | You.com becomes the backbone of the EU's AI strategy", "description": "As the EU's AI strategy is being debated, You.com becomes the backbone of the EU's AI strategy.", "page_age": "2025-06-25T11:41:00", "thumbnail_url": "https://www.somethumbnailsite.com/thumbnail.jpg", "url": "https://www.you.com/news/eu-ai-strategy-youcom"}]}, "metadata": {"search_uuid": "942ccbdd-7705-4d9c-9d37-4ef386658e90", "query": "What are the latest geopolitical updates from India", "latency": 0.123}} + "401": + application/json: {} + "403": + application/json: {} + "422": + application/json: {} + "500": + application/json: {} + getResearchTask: + speakeasy-default-get-research-task: + parameters: + path: + task_id: "586a9bc3-2c52-499c-a61d-be3cc9170c51" + responses: + "200": + application/json: {"id": "fedcb54a-4cd4-42fd-a550-113b9cb183ca", "task_type": "research", "status": "failed", "created_at": "2026-05-26T02:11:52.050Z", "updated_at": "2024-04-19T23:33:40.253Z"} + "401": + application/json: {} + "403": + application/json: {} + "404": + application/json: {"detail": "Task not found"} + "500": + application/json: {} + streamResearchTask: + speakeasy-default-stream-research-task: + parameters: + path: + task_id: "b431835b-e51d-453e-a623-25615ac31489" + query: + from_id: 0 + responses: + "401": + application/json: {} + "403": + application/json: {} + "404": + application/json: {"detail": "Task not found"} + "500": + application/json: {} + finance_research: + speakeasy-default-finance-research: + requestBody: + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} + responses: + "200": + application/json: {"output": {"content": "For fiscal year 2025, ended January 26, 2025, NVIDIA's revenue rose to **$130.5 billion, up 114% year over year**.[[1]]\nThe main drivers were Data Center demand (up 142%), Compute & Networking growth (up 145%), and smaller contributions from Gaming, Professional Visualization, and Automotive.", "content_type": "text", "sources": [{"url": "https://investor.apple.com/sec-filings/annual-reports/default.aspx", "title": "Apple Inc. Annual Report FY2024 (Form 10-K)"}]}} + "401": + application/json: {} + "403": + application/json: {} + "422": + application/json: {"detail": [{"type": "missing", "loc": ["body", "input"], "msg": "Field required", "input": ""}]} + "500": + application/json: {} + missingApiKey: + requestBody: + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} + responses: + "401": + application/json: {"detail": "API key is required"} + invalidOrExpired: + requestBody: + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} + responses: + "401": + application/json: {"detail": "Invalid or expired API key"} + otherAuthParsing: + requestBody: + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} + responses: + "401": + application/json: {"detail": ""} + missingScopes: + requestBody: + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} + responses: + "403": + application/json: {"detail": "Missing required scopes"} + missingField: + requestBody: + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} + responses: + "422": + application/json: {"detail": [{"type": "missing", "loc": ["body", "input"], "msg": "Field required", "input": {}}]} + invalidEnum: + requestBody: + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} + responses: + "422": + application/json: {"detail": [{"type": "enum", "loc": ["body", "research_effort"], "msg": "Input should be 'deep' or 'exhaustive'", "input": "invalid_value", "ctx": {"expected": "'deep' or 'exhaustive'"}}]} + stringTooLong: + requestBody: + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} responses: "422": application/json: {"detail": [{"type": "string_too_long", "loc": ["body", "input"], "msg": "String should have at most 40000 characters", "input": "", "ctx": {"max_length": 40000}}]} invalidJson: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} responses: "422": application/json: {"detail": [{"type": "json_invalid", "loc": ["body", 115], "msg": "JSON decode error", "input": {}, "ctx": {"error": "Invalid control character at"}}]} authFailure: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} responses: "500": application/json: {"detail": "Internal authentication error"} authorizationFailure: requestBody: - application/json: {"input": "", "research_effort": "standard"} + application/json: {"input": "What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", "research_effort": "deep"} responses: "500": application/json: {"detail": "Internal authorization error"} diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index f2ffbc8..48c79d3 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -34,7 +34,7 @@ generation: examples: - usage.md python: - version: 2.3.0 + version: 2.4.0 additionalDependencies: dev: {} main: {} @@ -46,13 +46,19 @@ python: authors: - You.com baseErrorName: YouError + bodyVariantOverloads: false clientServerStatusCodesAsErrors: true constFieldCasing: upper defaultErrorName: YouDefaultError description: The official You.com Python SDK. + durationFormat: false enableCustomCodeRegions: false enumFormat: enum envVarPrefix: YOU + errorSchemaValidation: true + eventStreamClassNames: + async: EventStreamAsync + sync: EventStream fixFlags: asyncPaginationSep2025: true conflictResistantModelImportsFeb2026: false @@ -72,18 +78,25 @@ python: webhooks: "" inferUnionDiscriminators: true inputModelSuffix: input + inputTypedDictSuffix: TypedDict license: Apache-2.0 maxMethodParams: 999 methodArguments: infer-optional-args + methodTimeoutArgument: timeout-ms + methodTimeoutUnits: milliseconds moduleName: "" multipartArrayFormat: standard + optionalDependencies: {} outputModelSuffix: output packageManager: uv packageName: youdotcom preApplyUnionDiscriminators: true pytestFilterWarnings: [] pytestTimeout: 0 + rawResponseHelpers: false responseFormat: flat + responseSchemaValidation: true sseFlatResponse: false templateVersion: v2 useAsyncHooks: false + uuidFormat: false diff --git a/.speakeasy/out.openapi.yaml b/.speakeasy/out.openapi.yaml index 85b6237..6e887f5 100644 --- a/.speakeasy/out.openapi.yaml +++ b/.speakeasy/out.openapi.yaml @@ -1,16 +1,19 @@ openapi: 3.1.0 info: - title: You.com API + title: You.com Finance Research API description: |- Unified API for Express, Advanced, and Custom Agents from You.com Get the best search results from web and news sources Returns the HTML or Markdown of a target webpage - Multi-step reasoning with comprehensive research capabilities Comprehensive API for You.com services: - **Agents API**: Execute queries using Express, Advanced, and Custom AI agents + - **Research API**: In-depth, multi-step research with citations and sources + - **Finance Research API**: Finance-focused multi-step research with citations and sources - **Search API**: Get search results from web and news sources - **Contents API**: Retrieve and process web page content - version: 1.0.0 + Multi-step reasoning with comprehensive research capabilities + Finance-focused multi-step research with competitive accuracy at same price points and latencies as the Research API + version: 0.0.1 servers: - url: https://api.you.com x-fern-server-name: Production @@ -115,186 +118,145 @@ paths: application/json: schema: $ref: "#/components/schemas/AgentRuns422Response" + servers: + - url: https://api.you.com tags: - agents.runs x-speakeasy-name-override: create /v1/search: + post: + operationId: searchPost + summary: Returns a list of unified search results from web and news sources + description: |- + This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. + + `POST` is the recommended method when using complex parameters such as `include_domains` or `exclude_domains`. These fields accept JSON arrays in the request body, which is unambiguous and supports up to 500 domains per request—something that would exceed URL length limits with GET. Use GET for simple queries where HTTP cacheability matters. + servers: + - url: https://ydc-index.io + security: + - ApiKeyAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SearchRequestBody' + required: true + responses: + "200": + $ref: '#/components/responses/SearchSuccess' + "401": + $ref: '#/components/responses/Unauthorized' + "403": + $ref: '#/components/responses/Forbidden' + "422": + $ref: '#/components/responses/UnprocessableEntity' + "500": + $ref: '#/components/responses/InternalServerError' get: operationId: search summary: Returns a list of unified search results from web and news sources - description: This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. + description: |- + This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. + + `GET` is a good choice for simple queries where HTTP cacheability matters—GET responses can be cached at CDN and proxy layers, whereas POST responses are not cached by default per the HTTP spec. For requests with complex parameters such as `include_domains` or `exclude_domains`, use POST instead - domain lists are passed as comma-separated strings in GET and are limited by URL length. + servers: + - url: https://ydc-index.io security: - ApiKeyAuth: [] parameters: - - name: query - in: query - description: The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. - required: true - schema: - type: string - default: "Your query" - example: "Your query" - - name: count - in: query - description: Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). - required: false - schema: - type: integer - maximum: 100 - minimum: 1 - default: 10 - - name: freshness - in: query - description: |- - Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. - - When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. - required: false - schema: - oneOf: - - $ref: '#/components/schemas/Freshness' - - type: string - - name: offset - in: query - description: Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. - required: false - schema: - type: integer - - name: country - in: query - description: The country code that determines the geographical focus of the web results. - required: false - schema: - oneOf: - - $ref: '#/components/schemas/Country' - - type: string - - name: language - in: query - description: The language of the web results that will be returned (BCP 47 format). - required: false - schema: - $ref: '#/components/schemas/Language' - - name: safesearch - in: query - description: Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. - required: false - schema: - oneOf: - - $ref: '#/components/schemas/SafeSearch' - - type: string - - name: livecrawl - in: query - description: Indicates which section(s) of search results to livecrawl and return full page content. - required: false - schema: - oneOf: - - $ref: '#/components/schemas/LiveCrawl' - - type: string - - name: livecrawl_formats - in: query - description: Indicates the format of the livecrawled content. - required: false - schema: - oneOf: - - $ref: '#/components/schemas/LiveCrawlFormats' - - type: string + - $ref: '#/components/parameters/Query' + - $ref: '#/components/parameters/Count' + - $ref: '#/components/parameters/FreshnessParam' + - $ref: '#/components/parameters/Offset' + - $ref: '#/components/parameters/CountryParam' + - $ref: '#/components/parameters/LanguageParam' + - $ref: '#/components/parameters/SafeSearchParam' + - $ref: '#/components/parameters/LiveCrawlParam' + - $ref: '#/components/parameters/LiveCrawlFormatsParam' + - $ref: '#/components/parameters/IncludeDomainsParam' + - $ref: '#/components/parameters/ExcludeDomainsParam' + - $ref: '#/components/parameters/BoostDomainsParam' + - $ref: '#/components/parameters/CrawlTimeout' + responses: + "200": + $ref: '#/components/responses/SearchSuccess' + "401": + $ref: '#/components/responses/Unauthorized' + "403": + $ref: '#/components/responses/Forbidden' + "422": + $ref: '#/components/responses/UnprocessableEntity' + "500": + $ref: '#/components/responses/InternalServerError' + tags: + - search + x-speakeasy-name-override: unified + /v1/contents: + post: + operationId: contents + summary: Returns the content of the web pages + description: Returns the HTML or Markdown of a target webpage. + security: + - ApiKeyAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + urls: + type: array + items: + type: string + format: uri + example: "https://www.you.com" + description: Array of URLs to fetch the contents from. + formats: + $ref: '#/components/schemas/ContentsFormats' + crawl_timeout: + type: integer + maximum: 60 + minimum: 1 + description: Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. + default: 10 + example: 10 + max_age: + type: integer + minimum: 0 + description: 'Maximum allowed age of cached content in seconds. When set, cached content older than this threshold is ignored and the page is re-fetched. Must be 0 or greater. Default: null (no age limit, cached content is returned regardless of age).' + default: null + nullable: true + example: 86400 + required: true responses: "200": - description: A JSON object containing unified search results from web and news sources + description: An array of JSON objects containing the page content of each web page content: application/json: schema: - type: object - properties: - results: - type: object - properties: - web: - type: array - items: - type: object - properties: - url: - type: string - description: The URL of the specific search result. - example: "https://you.com" - title: - type: string - description: The title or name of the search result. - example: "The World's Greatest Search Engine!" - description: - type: string - description: A brief description of the content of the search result. - example: "Search on YDC" - snippets: - type: array - items: - type: string - example: "I'm an AI assistant that helps you get more done. What can I help you with?" - description: An array of text snippets from the search result, providing a preview of the content. - thumbnail_url: - type: string - description: URL of the thumbnail. - example: "https://www.somethumbnailsite.com/thumbnail.jpg" - page_age: - type: string - format: date-time - description: The age of the search result. - example: "2025-06-25T11:41:00" - contents: - $ref: '#/components/schemas/Contents' - authors: - type: array - items: - type: string - example: "John Doe" - description: An array of authors of the search result. - favicon_url: - type: string - description: The URL of the favicon of the search result's domain. - example: "https://someurl.com/favicon" - news: - type: array - items: - type: object - properties: - title: - type: string - description: The title of the news result. - example: "Exclusive | You.com becomes the backbone of the EU's AI strategy" - description: - type: string - description: A brief description of the content of the news result. - example: "As the EU's AI strategy is being debated, You.com becomes the backbone of the EU's AI strategy." - page_age: - type: string - format: date-time - description: UTC timestamp of the article's publication date. - example: "2025-06-25T11:41:00" - thumbnail_url: - type: string - description: URL of the thumbnail. - example: "https://www.somethumbnailsite.com/thumbnail.jpg" - url: - type: string - description: The URL of the news result. - example: "https://www.you.com/news/eu-ai-strategy-youcom" - contents: - $ref: '#/components/schemas/Contents' - metadata: - type: object - properties: - search_uuid: - type: string - format: uuid - example: "942ccbdd-7705-4d9c-9d37-4ef386658e90" - query: - type: string - description: Returns the search query used to retrieve the results. - example: "Your query" - latency: - type: number - example: 0.123 + type: array + items: + type: object + properties: + url: + type: string + format: uri + description: The webpage URL whose content has been fetched. + example: "https://www.you.com" + title: + type: string + description: The title of the web page. + example: "The best website in the world" + html: + type: string + description: The retrieved HTML content of the web page. + nullable: true + markdown: + type: string + description: The retrieved Markdown content of the web page. + nullable: true + metadata: + $ref: '#/components/schemas/ContentsMetadata' "401": description: Unauthorized. Problems with API key. content: @@ -351,67 +313,222 @@ paths: value: detail: "Internal authorization error" tags: - - search - x-speakeasy-name-override: unified + - contents + x-speakeasy-name-override: generate servers: - url: https://ydc-index.io - /v1/contents: + /v1/research: post: - operationId: contents - summary: Returns the content of the web pages - description: Returns the HTML or Markdown of a target webpage. + operationId: research + summary: Returns comprehensive research-grade answers with multi-step reasoning + description: Research goes beyond a single web search. In response to your question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust and verify. + security: + - ApiKeyAuth: [] requestBody: content: application/json: schema: type: object properties: - urls: - type: array - items: - type: string - format: uri - example: "https://www.you.com" - description: Array of URLs to fetch the contents from. - formats: - $ref: '#/components/schemas/ContentsFormats' - crawl_timeout: - type: integer - maximum: 60 - minimum: 1 - description: Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. - default: 10 - example: 10 + input: + type: string + maxLength: 40000 + description: |- + The research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + research_effort: + $ref: '#/components/schemas/ResearchEffort' + background: + type: boolean + description: When true, queue a research task and return a task handle immediately instead of waiting for the result inline. Defaults to synchronous. When enabled, the response is a TaskResponse object with a task_id and stream_url for polling progress via GET /v1/research/{task_id} or streaming via GET /v1/research/{task_id}/stream. + default: false + source_control: + type: object + properties: + include_domains: + type: array + items: + type: string + description: Only return results from these domains. Max 500 domains. Cannot be used with exclude_domains or boost_domains. + exclude_domains: + type: array + items: + type: string + description: Never return results from these domains. Max 500 domains. Also blocks the research agent from visiting pages on those domains during browsing. + boost_domains: + type: array + items: + type: string + description: Boost results from these domains without excluding other domains. Max 500 domains. Cannot be used with include_domains. + freshness: + type: string + description: Filter results by recency. Accepts `day`, `week`, `month`, `year`, or a custom date range in `YYYY-MM-DDtoYYYY-MM-DD` format. + country: + type: string + description: ISO 3166-1 alpha-2 country code, such as US, GB, or DE, to geographically focus web results. + description: |- + Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country. + + `include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. + output_schema: + type: object + description: |- + Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: "lite" returns 422. + + Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported. + + Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. + required: + - input + example: + input: Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed? + research_effort: lite required: true responses: "200": - description: An array of JSON objects containing the page content of each web page + description: A JSON object containing a comprehensive answer with citations and supporting search results. When background=true, returns a task handle instead. content: application/json: schema: - type: array - items: - type: object - properties: - url: - type: string - format: uri - description: The webpage URL whose content has been fetched. - example: "https://www.you.com" - title: - type: string - description: The title of the web page. - example: "The best website in the world" - html: - type: string - description: The retrieved HTML content of the web page. - nullable: true - markdown: - type: string - description: The retrieved Markdown content of the web page. - nullable: true - metadata: - $ref: '#/components/schemas/ContentsMetadata' + oneOf: + - $ref: '#/components/schemas/ResearchResponse' + - $ref: '#/components/schemas/TaskResponse' + example: + output: + content: |- + Over the past decade, some global cities have shown improvements in air quality due to specific actions. Beijing, for example, made significant strides in improving its air quality through coordinated control measures with surrounding areas, collaborative planning, unified standards, joint emergency responses, and information sharing [[1]]. These efforts, including a five-year action plan for air pollution prevention and control, have helped to substantially improve air quality in the Jing-Jin-Ji region [[2]]. + + Paris has also seen improvements, with a 50% reduction in Nitrogen dioxide pollution and a 55% decrease in particulate matter citywide since 2005. This was achieved through its climate strategy, which included adding more bike lanes and increasing cycling networks [[3]]. Wellington, New Zealand, improved air quality in one of its busiest districts by increasing the percentage of electric buses from 5% to over 50% between 2022 and 2023, leading to a 50% reduction in black carbon and a 29% drop in nitrogen dioxide levels [[3]]. Mexico City, once known as the world's most polluted city in the early 1990s, has vastly improved its air quality, with the daily concentration of SO2 declining significantly by 2018 [[2]]. + + While many cities globally have experienced persistently high or even rising levels of air pollution, especially concerning PM2.5 concentrations, NO2 exposures have shown an encouraging trend, with 211 more cities meeting the WHO guideline in 2019 compared to 2010 [[4]]. Local policies have been instrumental in these improvements [[4]]. + content_type: text + sources: + - url: https://sustainablemobility.iclei.org/air-pollution-beijing/ + title: "Clearing the skies: how Beijing tackled air pollution & what lies ..." + snippets: + - >- + However, Beijing has made remarkable strides in recent years to improve its air quality, setting an example for other cities grappling with similar challenges. The root causes Comparing the past 20 years of its development to the 20 before, Beijing's GDP, population, and vehicles sharply increased by 1078%, 74%, and 335% respectively (UNEP, 2019). + - >- + The city actively coordinated air pollution control measures with surrounding areas, such as the Beijing-Tianjin-Hebei region. Collaborative planning, unified standards, joint emergency responses, and information sharing significantly improved the air quality in this broader region. + - >- + While Beijing has made significant strides, challenges remain. The average PM2.5 level is still six times higher than the World Health Organization's (WHO) guideline, and the 2021-22 improvement may be partially attributed to measures taken for the Winter Olympics. + - >- + As China emerged as the world's largest automobile producer and consumer, it grappled with the detrimental impacts of increased oil consumption. Furthermore, the high level of coal consumption, especially during the winter heating season, contributed to the city's deteriorating air quality, reaching an average of 101.56 micrograms of PM2.5 particles per cubic meter in 2013 (Statista, 2023). + - url: https://blogs.worldbank.org/en/voices/tackling-poor-air-quality-lessons-three-cities + title: "Tackling poor air quality: Lessons from three cities" + snippets: + - >- + The latest World Bank report, Clearing the Air: A Tale of Three Cities, chose Beijing, New Delhi and Mexico City to assess how current and past efforts improved air quality. In the early 1990s, Mexico City was known as the world's most polluted city and while there are still challenges, air quality has vastly improved. Daily concentration of SO2 – a contributor to PM2.5 concentrations – declined from 300 µg/m3 in the 1990s to less than 100 µg/m3 in 2018. + - >- + In China, the ministries of Environmental Protection (now the Ministry of Ecology and Environment), Industry and Information Technology, Finance, Housing and Rural Development, along with the National Development and Reform Commission and National Energy Administration, worked together to issue a five-year action plan for air pollution prevention and control for the entire Jing-Jin-Ji region that surrounds Beijing and includes the municipality of Beijing, municipality of Tianjin, the province of Hebei, and small parts of Henan, Shanxi, inner Mongolia, and Shandong. What's encouraging about this new work is that it shows that with the right policies, incentives and information, air quality can be improved substantially, particularly as countries work to grow back cleaner after the pandemic. + - >- + Failure to provide such incentives in India in the late 1990s resulted in the government developing plans but not implementing them. This led to India's Supreme Court stepping in to force the government to implement policy measures. A recent government of India program to provide performance-based grants to cities to reward improvements in air quality is a step in the right direction. + - >- + The cost associated with health impacts of outdoor PM2.5 air pollution is estimated to be US$5.7 trillion, equivalent to 4.8 percent of global GDP, according to World Bank research. The COVID-19 pandemic further highlights why addressing air pollution is so important, with early research pointing to links between air pollution, illness and death due to the virus. On the flip side, the economic lockdowns caused by the pandemic, while devastating for communities, did result in some noticeable improvements in air quality but these improvements were inconsistent, particularly when it came to PM2.5. + - url: https://www.weforum.org/stories/2025/06/urban-mobility-improving-cities-air-quality/ + title: "Boosting clean air strategies in cities around the world | World ..." + snippets: + - >- + Comprehensive cycling networks improve air quality while also transforming urban mobility. Paris has added more bike lanes to its cityscape in recent years. Between 2022 and 2023 alone, bike path usage doubled during rush hour and cyclists now outnumber cars on many of the city's streets. The results of Paris' growing cycling network are promising. Alongside other elements of Paris's climate strategy, cycling has contributed to a 50% reduction in Nitrogen dioxide pollution and 55% decrease in particulate matter citywide since 2005. + - >- + In 2025, the alliance and members of the Global New Mobility Coalition will launch a new workstream on Transport and Urbanism that aims to speed up cross-sector collaboration on implementing proven mobility options to improve air quality and drive sustainable growth. + - >- + Air pollution has been estimated to cause 4.2 million premature deaths worldwide per year, according to the World Health Organization, and nearly half of urban airborne contamination comes from city transport. While vehicles are essential to the vitality of cities, without the right policies in place, transport will continue to be a major contributor to harmful air pollution. + - >- + In Wellington, New Zealand, the percentage of electric buses travelling across the city's heavily trafficked Golden Mile corridor rose from 5% to over 50% between 2022 and 2023. This shift led to a 50% reduction in black carbon and a 29% drop in nitrogen dioxide levels throughout the district. This has significantly improved air quality in one of the busiest parts of Wellington, as well as lowering noise pollution. + - url: https://www.stateofglobalair.org/resources/health-in-cities + title: "Air Pollution and Health in Cities | State of Global Air" + snippets: + - >- + Globally, NO2 exposures are heading in an encouraging direction as 211 more cities met the WHO guideline of 10 µg/m3 in 2019 compared to 2010. However, NO2 pollution is worsening in some other regions. Percentage of cities by population-weighted annual average pollutant concentration in 2010 and 2019. However, interventions targeting pollution at the local scale have successfully improved air quality in some cities. + - >- + Local policies have improved air quality in some cities, while pollution has worsened in others. Overall, many cities have seen persistently high — and even rising — levels of air pollution over the past decade. PM2.5 exposures remained stagnant in many cities from 2010 to 2019. + - >- + Cities are often hotspots for poor air quality. As rapid urbanization increases the number of people breathing dangerously polluted air, city-level data can help inform targeted efforts to curb urban air pollution and improve public health. + - >- + Explore air quality and health data for your city using our new interactive app here. Most cities have polluted air, but the type of pollution varies from place to place. Local policies have improved air quality in some cities, while pollution has worsened in others. + "422": + description: Unprocessable Entity. Request validation failed. + content: + application/json: + schema: + type: object + properties: + detail: + type: array + items: + type: object + properties: + type: + type: string + description: The validation error type. + example: missing + loc: + type: array + items: + oneOf: + - type: string + - type: integer + description: The location of the error as a path of segments (strings for field names, integers for byte offsets). + example: ["body", "input"] + msg: + type: string + description: A human-readable description of the error. + example: Field required + input: + oneOf: + - type: string + - type: object + description: The input value that caused the error. + ctx: + type: object + additionalProperties: true + description: Additional context about the error. + required: + - type + - loc + - msg + - input + examples: + missingField: + summary: Required field missing + value: + detail: + - type: missing + loc: ["body", "input"] + msg: Field required + input: {} + invalidEnum: + summary: Invalid enum value for research_effort + value: + detail: + - type: enum + loc: ["body", "research_effort"] + msg: "Input should be 'lite', 'standard', 'deep' or 'exhaustive'" + input: invalid_value + ctx: + expected: "'lite', 'standard', 'deep' or 'exhaustive'" + stringTooLong: + summary: Input exceeds maximum length + value: + detail: + - type: string_too_long + loc: ["body", "input"] + msg: String should have at most 40000 characters + input: + ctx: + max_length: 40000 + invalidJson: + summary: Invalid JSON body + value: + detail: + - type: json_invalid + loc: ["body", 115] + msg: JSON decode error + input: {} + ctx: + error: Invalid control character at "401": description: Unauthorized. Problems with API key. content: @@ -426,15 +543,163 @@ paths: missingApiKey: summary: Missing API key value: - detail: "API key is required" + detail: API key is required invalidOrExpired: summary: Invalid/expired API key value: - detail: "Invalid or expired API key" + detail: Invalid or expired API key otherAuthParsing: summary: Other auth parsing errors value: - detail: "" + detail: + "403": + description: Forbidden. API key lacks scope for this path. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + examples: + missingScopes: + summary: Missing required scopes + value: + detail: Missing required scopes + "500": + description: Internal Server Error during authentication/authorization middleware. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + examples: + authFailure: + summary: Authentication failure + value: + detail: Internal authentication error + authorizationFailure: + summary: Authorization failure + value: + detail: Internal authorization error + /v1/research/{task_id}: + get: + operationId: getResearchTask + summary: Get the status of a background research task + description: Poll the status of a background research task created with background=true. When the task is completed, the result is included in the response. + security: + - ApiKeyAuth: [] + parameters: + - name: task_id + in: path + description: The UUID of the research task. + required: true + schema: + type: string + format: uuid + responses: + "200": + description: The task status and, when complete, the result. + content: + application/json: + schema: + $ref: '#/components/schemas/TaskDetail' + example: + id: "a1b2c3d4-0000-0000-0000-000000000000" + task_type: research + status: completed + created_at: "2026-07-09T18:00:00Z" + updated_at: "2026-07-09T18:02:30Z" + completed_at: "2026-07-09T18:02:30Z" + error: null + input: + input: "What are the tradeoffs between microservices and monolithic architectures?" + research_effort: deep + result: + output: + content: "## Microservices vs Monolithic Architectures\n\nThe choice involves several key tradeoffs..." + content_type: text + sources: + - url: https://example.com/architecture-patterns + title: "Architecture Patterns for High-Traffic Systems" + "401": + description: Unauthorized. Problems with API key. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + "403": + description: Forbidden. API key lacks scope for this path. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + "404": + description: Task not found or not authorized. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + example: Task not found + "500": + description: Internal Server Error. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + /v1/research/{task_id}/stream: + get: + operationId: streamResearchTask + summary: Stream updates for a background research task + description: Stream real-time updates for a background research task via Server-Sent Events (SSE). Supports reconnection via the from_id query parameter to replay missed events. The connection closes automatically when the task reaches a terminal state. + security: + - ApiKeyAuth: [] + parameters: + - name: task_id + in: path + description: The UUID of the research task. + required: true + schema: + type: string + format: uuid + - name: from_id + in: query + description: Resume from a sequence number for reconnection. + required: false + schema: + type: integer + minimum: 0 + default: 0 + responses: + "200": + description: SSE stream of task events. + content: + text/event-stream: + schema: + $ref: '#/components/schemas/ResearchTaskStreamEvent' + "401": + description: Unauthorized. Problems with API key. + content: + application/json: + schema: + type: object + properties: + detail: + type: string "403": description: Forbidden. API key lacks scope for this path. content: @@ -444,13 +709,18 @@ paths: properties: detail: type: string - examples: - missingScopes: - summary: Missing required scopes - value: - detail: "Missing required scopes" + "404": + description: Task not found or not authorized. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + example: Task not found "500": - description: Internal Server Error during authentication/authorization middleware. + description: Internal Server Error. content: application/json: schema: @@ -458,25 +728,13 @@ paths: properties: detail: type: string - examples: - authFailure: - summary: Authentication failure - value: - detail: "Internal authentication error" - authorizationFailure: - summary: Authorization failure - value: - detail: "Internal authorization error" - tags: - - contents - x-speakeasy-name-override: generate - servers: - - url: https://ydc-index.io - /v1/research: + /v1/finance_research: post: - operationId: research - summary: Returns comprehensive research-grade answers with multi-step reasoning - description: Research goes beyond a single web search. In response to your question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust and verify. + operationId: finance_research + summary: Returns comprehensive finance-grade research answers with multi-step reasoning + description: |- + The Finance Research API is purpose-built for financial questions. Like the Research API, it runs multiple searches, reads through sources, and synthesizes everything into a thorough, well-cited answer — but its retrieval index is optimized for financial data: earnings reports, SEC filings, analyst coverage, market data, and financial news. + Use it when you need credible, sourced answers to financial questions: company fundamentals, market trends, competitive analysis, earnings summaries, or macroeconomic research. security: - ApiKeyAuth: [] requestBody: @@ -489,34 +747,21 @@ paths: type: string maxLength: 40000 description: |- - The research question or complex query requiring in-depth investigation and multi-step reasoning. + The financial research question or complex query requiring in-depth investigation and multi-step reasoning. Note: The maximum length of the input is 40,000 characters. + example: What were the key drivers of NVIDIA's revenue growth in fiscal year 2025? research_effort: - type: string - enum: - - lite - - standard - - deep - - exhaustive - description: |- - Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. - - Available levels: - - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. - - `standard`: The default. Balances speed and depth, a good fit for most questions. - - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. - - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. - default: standard + $ref: '#/components/schemas/FinanceResearchEffort' required: - input example: - input: Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed? - research_effort: lite + input: What were the key drivers of NVIDIA's revenue growth in fiscal year 2025? + research_effort: deep required: true responses: "200": - description: A JSON object containing a comprehensive answer with citations and supporting search results + description: A JSON object containing a comprehensive finance-grade answer with citations and supporting search results content: application/json: schema: @@ -527,12 +772,16 @@ paths: properties: content: type: string - description: The comprehensive response with inline citations. The content is formatted in Markdown and includes numbered citations that reference the items in the sources array. + description: The comprehensive finance-grade response with inline citations. Content is a Markdown string with numbered citations that reference the items in the sources array. + example: |- + For fiscal year 2025, ended January 26, 2025, NVIDIA's revenue rose to **$130.5 billion, up 114% year over year**.[[1]] + The main drivers were Data Center demand (up 142%), Compute & Networking growth (up 145%), and smaller contributions from Gaming, Professional Visualization, and Automotive. content_type: type: string enum: - text description: The format of the content field. + example: text sources: type: array items: @@ -541,14 +790,11 @@ paths: url: type: string description: The URL of the source webpage. + example: https://investor.apple.com/sec-filings/annual-reports/default.aspx title: type: string description: The title of the source webpage. - snippets: - type: array - items: - type: string - description: Relevant excerpts from the source page that were used in generating the answer. + example: "Apple Inc. Annual Report FY2024 (Form 10-K)" required: - url description: A list of web sources used to generate the answer. @@ -562,57 +808,17 @@ paths: example: output: content: |- - Over the past decade, some global cities have shown improvements in air quality due to specific actions. Beijing, for example, made significant strides in improving its air quality through coordinated control measures with surrounding areas, collaborative planning, unified standards, joint emergency responses, and information sharing [[1]]. These efforts, including a five-year action plan for air pollution prevention and control, have helped to substantially improve air quality in the Jing-Jin-Ji region [[2]]. + For fiscal year 2025, ended January 26, 2025, NVIDIA's revenue rose to **$130.5 billion, up 114% year over year**.[[1]] The main drivers were: - Paris has also seen improvements, with a 50% reduction in Nitrogen dioxide pollution and a 55% decrease in particulate matter citywide since 2005. This was achieved through its climate strategy, which included adding more bike lanes and increasing cycling networks [[3]]. Wellington, New Zealand, improved air quality in one of its busiest districts by increasing the percentage of electric buses from 5% to over 50% between 2022 and 2023, leading to a 50% reduction in black carbon and a 29% drop in nitrogen dioxide levels [[3]]. Mexico City, once known as the world's most polluted city in the early 1990s, has vastly improved its air quality, with the daily concentration of SO2 declining significantly by 2018 [[2]]. + - **Data Center demand**, especially accelerated computing and AI platforms: Data Center revenue was **up 142%**, driven by demand for NVIDIA's **Hopper architecture** for large language models, recommendation engines, and generative AI applications; NVIDIA also began shipping **Blackwell** production systems in Q4 FY2025.[[1]] + - **Compute & Networking segment growth**: revenue increased **145%**, with Data Center compute up **162%** on Hopper demand and Data Center networking up **51%**, driven by Ethernet for AI, including Spectrum-X.[[1]] + - Smaller but positive contributions from other markets: **Gaming revenue rose 9%** on GeForce RTX 40 Series GPUs, **Professional Visualization rose 21%** on Ada RTX workstation adoption, and **Automotive rose 55%** from self-driving platform sales.[[1]] - While many cities globally have experienced persistently high or even rising levels of air pollution, especially concerning PM2.5 concentrations, NO2 exposures have shown an encouraging trend, with 211 more cities meeting the WHO guideline in 2019 compared to 2010 [[4]]. Local policies have been instrumental in these improvements [[4]]. + In short, NVIDIA's FY2025 growth was overwhelmingly driven by **AI-related Data Center compute and networking demand**, with additional support from gaming, professional visualization, and automotive. content_type: text sources: - - url: https://sustainablemobility.iclei.org/air-pollution-beijing/ - title: "Clearing the skies: how Beijing tackled air pollution & what lies ..." - snippets: - - >- - However, Beijing has made remarkable strides in recent years to improve its air quality, setting an example for other cities grappling with similar challenges. The root causes Comparing the past 20 years of its development to the 20 before, Beijing's GDP, population, and vehicles sharply increased by 1078%, 74%, and 335% respectively (UNEP, 2019). - - >- - The city actively coordinated air pollution control measures with surrounding areas, such as the Beijing-Tianjin-Hebei region. Collaborative planning, unified standards, joint emergency responses, and information sharing significantly improved the air quality in this broader region. - - >- - While Beijing has made significant strides, challenges remain. The average PM2.5 level is still six times higher than the World Health Organization's (WHO) guideline, and the 2021-22 improvement may be partially attributed to measures taken for the Winter Olympics. - - >- - As China emerged as the world's largest automobile producer and consumer, it grappled with the detrimental impacts of increased oil consumption. Furthermore, the high level of coal consumption, especially during the winter heating season, contributed to the city's deteriorating air quality, reaching an average of 101.56 micrograms of PM2.5 particles per cubic meter in 2013 (Statista, 2023). - - url: https://blogs.worldbank.org/en/voices/tackling-poor-air-quality-lessons-three-cities - title: "Tackling poor air quality: Lessons from three cities" - snippets: - - >- - The latest World Bank report, Clearing the Air: A Tale of Three Cities, chose Beijing, New Delhi and Mexico City to assess how current and past efforts improved air quality. In the early 1990s, Mexico City was known as the world's most polluted city and while there are still challenges, air quality has vastly improved. Daily concentration of SO2 – a contributor to PM2.5 concentrations – declined from 300 µg/m3 in the 1990s to less than 100 µg/m3 in 2018. - - >- - In China, the ministries of Environmental Protection (now the Ministry of Ecology and Environment), Industry and Information Technology, Finance, Housing and Rural Development, along with the National Development and Reform Commission and National Energy Administration, worked together to issue a five-year action plan for air pollution prevention and control for the entire Jing-Jin-Ji region that surrounds Beijing and includes the municipality of Beijing, municipality of Tianjin, the province of Hebei, and small parts of Henan, Shanxi, inner Mongolia, and Shandong. What's encouraging about this new work is that it shows that with the right policies, incentives and information, air quality can be improved substantially, particularly as countries work to grow back cleaner after the pandemic. - - >- - Failure to provide such incentives in India in the late 1990s resulted in the government developing plans but not implementing them. This led to India's Supreme Court stepping in to force the government to implement policy measures. A recent government of India program to provide performance-based grants to cities to reward improvements in air quality is a step in the right direction. - - >- - The cost associated with health impacts of outdoor PM2.5 air pollution is estimated to be US$5.7 trillion, equivalent to 4.8 percent of global GDP, according to World Bank research. The COVID-19 pandemic further highlights why addressing air pollution is so important, with early research pointing to links between air pollution, illness and death due to the virus. On the flip side, the economic lockdowns caused by the pandemic, while devastating for communities, did result in some noticeable improvements in air quality but these improvements were inconsistent, particularly when it came to PM2.5. - - url: https://www.weforum.org/stories/2025/06/urban-mobility-improving-cities-air-quality/ - title: "Boosting clean air strategies in cities around the world | World ..." - snippets: - - >- - Comprehensive cycling networks improve air quality while also transforming urban mobility. Paris has added more bike lanes to its cityscape in recent years. Between 2022 and 2023 alone, bike path usage doubled during rush hour and cyclists now outnumber cars on many of the city's streets. The results of Paris' growing cycling network are promising. Alongside other elements of Paris's climate strategy, cycling has contributed to a 50% reduction in Nitrogen dioxide pollution and 55% decrease in particulate matter citywide since 2005. - - >- - In 2025, the alliance and members of the Global New Mobility Coalition will launch a new workstream on Transport and Urbanism that aims to speed up cross-sector collaboration on implementing proven mobility options to improve air quality and drive sustainable growth. - - >- - Air pollution has been estimated to cause 4.2 million premature deaths worldwide per year, according to the World Health Organization, and nearly half of urban airborne contamination comes from city transport. While vehicles are essential to the vitality of cities, without the right policies in place, transport will continue to be a major contributor to harmful air pollution. - - >- - In Wellington, New Zealand, the percentage of electric buses travelling across the city's heavily trafficked Golden Mile corridor rose from 5% to over 50% between 2022 and 2023. This shift led to a 50% reduction in black carbon and a 29% drop in nitrogen dioxide levels throughout the district. This has significantly improved air quality in one of the busiest parts of Wellington, as well as lowering noise pollution. - - url: https://www.stateofglobalair.org/resources/health-in-cities - title: "Air Pollution and Health in Cities | State of Global Air" - snippets: - - >- - Globally, NO2 exposures are heading in an encouraging direction as 211 more cities met the WHO guideline of 10 µg/m3 in 2019 compared to 2010. However, NO2 pollution is worsening in some other regions. Percentage of cities by population-weighted annual average pollutant concentration in 2010 and 2019. However, interventions targeting pollution at the local scale have successfully improved air quality in some cities. - - >- - Local policies have improved air quality in some cities, while pollution has worsened in others. Overall, many cities have seen persistently high — and even rising — levels of air pollution over the past decade. PM2.5 exposures remained stagnant in many cities from 2010 to 2019. - - >- - Cities are often hotspots for poor air quality. As rapid urbanization increases the number of people breathing dangerously polluted air, city-level data can help inform targeted efforts to curb urban air pollution and improve public health. - - >- - Explore air quality and health data for your city using our new interactive app here. Most cities have polluted air, but the type of pollution varies from place to place. Local policies have improved air quality in some cities, while pollution has worsened in others. + - url: https://investor.nvidia.com/financial-info/financial-reports/default.aspx + title: NVIDIA Corporation - Financial Reports "422": description: Unprocessable Entity. Request validation failed. content: @@ -670,10 +876,10 @@ paths: detail: - type: enum loc: ["body", "research_effort"] - msg: "Input should be 'lite', 'standard', 'deep' or 'exhaustive'" + msg: "Input should be 'deep' or 'exhaustive'" input: invalid_value ctx: - expected: "'lite', 'standard', 'deep' or 'exhaustive'" + expected: "'deep' or 'exhaustive'" stringTooLong: summary: Input exceeds maximum length value: @@ -755,7 +961,7 @@ components: type: apiKey in: header name: X-API-Key - description: "A unique API Key is required to authorize API access. [Get your API Key with free credits](https://you.com/platform/api-keys)." + description: "A unique API Key is required to authorize API access. [Get your API Key with free credits](https://you.com/platform)." schemas: # REQUEST SCHEMAS ExpressAgentRunsRequest: @@ -1257,6 +1463,16 @@ components: - loc - msg - input + SearchQuery: + type: string + description: The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. + example: What are the latest geopolitical updates from India + Count: + type: integer + maximum: 100 + minimum: 1 + description: Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). + default: 10 Freshness: type: string enum: @@ -1264,102 +1480,115 @@ components: - week - month - year - description: Specifies the freshness of the results to return. + FreshnessValue: + oneOf: + - $ref: '#/components/schemas/Freshness' + - type: string + description: |- + Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. + Offset: + type: integer + maximum: 9 + minimum: 0 + description: Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. Country: type: string enum: - - "AR" - - "AU" - - "AT" - - "BE" - - "BR" - - "CA" - - "CL" - - "DK" - - "FI" - - "FR" - - "DE" - - "HK" - - "IN" - - "ID" - - "IT" - - "JP" - - "KR" - - "MY" - - "MX" - - "NL" - - "NZ" - - "NO" - - "CN" - - "PL" - - "PT" - - "PH" - - "RU" - - "SA" - - "ZA" - - "ES" - - "SE" - - "CH" - - "TW" - - "TR" - - "GB" - - "US" + - AR + - AU + - AT + - BE + - BR + - CA + - CL + - DK + - FI + - FR + - DE + - HK + - IN + - ID + - IT + - JP + - KR + - MY + - MX + - NL + - NZ + - NO + - CN + - PL + - PT + - PH + - RU + - SA + - ZA + - ES + - SE + - CH + - TW + - TR + - GB + - US description: The country code that determines the geographical focus of the web results. Language: type: string enum: - - "AR" - - "EU" - - "BN" - - "BG" - - "CA" - - "ZH-HANS" - - "ZH-HANT" - - "HR" - - "CS" - - "DA" - - "NL" - - "EN" - - "EN-GB" - - "ET" - - "FI" - - "FR" - - "GL" - - "DE" - - "EL" - - "GU" - - "HE" - - "HI" - - "HU" - - "IS" - - "IT" - - "JP" - - "KN" - - "KO" - - "LV" - - "LT" - - "MS" - - "ML" - - "MR" - - "NB" - - "PL" - - "PT-BR" - - "PT-PT" - - "PA" - - "RO" - - "RU" - - "SR" - - "SK" - - "SL" - - "ES" - - "SV" - - "TA" - - "TE" - - "TH" - - "TR" - - "UK" - - "VI" - default: "EN" + - AR + - EU + - BN + - BG + - CA + - ZH-HANS + - ZH-HANT + - HR + - CS + - DA + - NL + - EN + - EN-GB + - ET + - FI + - FR + - GL + - DE + - EL + - GU + - HE + - HI + - HU + - IS + - IT + - JA + - KN + - KO + - LV + - LT + - MS + - ML + - MR + - NB + - PL + - PT-BR + - PT-PT + - PA + - RO + - RU + - SR + - SK + - SL + - ES + - SV + - TA + - TE + - TH + - TR + - UK + - VI + description: The language of the web results that will be returned (BCP 47 format). + default: EN SafeSearch: type: string enum: @@ -1375,11 +1604,182 @@ components: - all description: Indicates which section(s) of search results to livecrawl and return full page content. LiveCrawlFormats: - type: string - enum: - - html - - markdown - description: Indicates the format of the livecrawled content. + type: array + items: + type: string + enum: + - html + - markdown + description: 'Indicates the format(s) of the livecrawled content. Pass one or both values (`html`, `markdown`). In a GET request, repeat the parameter: `?livecrawl_formats=html&livecrawl_formats=markdown`. In a POST body, provide a JSON array: `["html", "markdown"]`.' + IncludeDomains: + type: array + items: + type: string + description: |- + A list of domains to restrict search results to. Only results from these domains will be returned. Supports up to 500 domains. This is a strict allowlist, not a boost — results are limited exclusively to the specified domains. + + Cannot be combined with `exclude_domains`; passing both will return a `422` error. + example: + - nytimes.com + - bbc.com + ExcludeDomains: + type: array + items: + type: string + description: |- + A list of domains to exclude from search results. Results from these domains will be filtered out. Supports up to 500 domains. + + Cannot be combined with `include_domains`; passing both will return a `422` error. + example: + - spam-site.com + - other-site.com + BoostDomains: + type: array + items: + type: string + description: A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). + example: + - nytimes.com + - wired.com + CrawlTimeout: + type: integer + maximum: 60 + minimum: 1 + description: Maximum time in seconds to wait for page content when `livecrawl` is enabled. Must be between 1 and 60 seconds. Default is 10 seconds. + default: 10 + example: 10 + SearchRequestBody: + type: object + properties: + query: + $ref: '#/components/schemas/SearchQuery' + count: + $ref: '#/components/schemas/Count' + freshness: + $ref: '#/components/schemas/FreshnessValue' + offset: + $ref: '#/components/schemas/Offset' + country: + $ref: '#/components/schemas/Country' + language: + $ref: '#/components/schemas/Language' + safesearch: + $ref: '#/components/schemas/SafeSearch' + livecrawl: + $ref: '#/components/schemas/LiveCrawl' + livecrawl_formats: + $ref: '#/components/schemas/LiveCrawlFormats' + include_domains: + $ref: '#/components/schemas/IncludeDomains' + exclude_domains: + $ref: '#/components/schemas/ExcludeDomains' + boost_domains: + $ref: '#/components/schemas/BoostDomains' + crawl_timeout: + $ref: '#/components/schemas/CrawlTimeout' + required: + - query + SearchResponse: + type: object + properties: + results: + type: object + properties: + web: + type: array + items: + $ref: '#/components/schemas/WebResult' + news: + type: array + items: + $ref: '#/components/schemas/NewsResult' + metadata: + $ref: '#/components/schemas/SearchMetadata' + WebResult: + type: object + properties: + url: + type: string + description: The URL of the specific search result. + example: https://you.com + title: + type: string + description: The title or name of the search result. + example: The World's Greatest Search Engine! + description: + type: string + description: A brief description of the content of the search result. + example: Search on YDC + snippets: + type: array + items: + type: string + example: >- + I'm an AI assistant that helps you get more done. What can I help you with? + description: An array of text snippets from the search result, providing a preview of the content. + thumbnail_url: + type: string + description: URL of the thumbnail. + example: https://www.somethumbnailsite.com/thumbnail.jpg + page_age: + type: string + format: date-time + description: The age of the search result. + example: "2025-06-25T11:41:00" + contents: + $ref: '#/components/schemas/Contents' + authors: + type: array + items: + type: string + example: John Doe + description: An array of authors of the search result. + favicon_url: + type: string + description: The URL of the favicon of the search result's domain. + example: https://someurl.com/favicon + NewsResult: + type: object + properties: + title: + type: string + description: The title of the news result. + example: >- + Exclusive | You.com becomes the backbone of the EU's AI strategy + description: + type: string + description: A brief description of the content of the news result. + example: >- + As the EU's AI strategy is being debated, You.com becomes the backbone of the EU's AI strategy. + page_age: + type: string + format: date-time + description: UTC timestamp of the article's publication date. + example: "2025-06-25T11:41:00" + thumbnail_url: + type: string + description: URL of the thumbnail. + example: https://www.somethumbnailsite.com/thumbnail.jpg + url: + type: string + description: The URL of the news result. + example: https://www.you.com/news/eu-ai-strategy-youcom + contents: + $ref: '#/components/schemas/Contents' + SearchMetadata: + type: object + properties: + search_uuid: + type: string + format: uuid + example: 942ccbdd-7705-4d9c-9d37-4ef386658e90 + query: + type: string + description: Returns the search query used to retrieve the results. + example: What are the latest geopolitical updates from India + latency: + type: number + example: 0.123 Contents: type: object properties: @@ -1414,5 +1814,382 @@ components: example: "https://api.ydc-index.io/favicon?domain=you.com&size=128" description: Metadata about the web page. Only returned when 'metadata' is included in the formats array. nullable: true -security: - - ApiKeyAuth: [] + ResearchEffort: + type: string + enum: + - lite + - standard + - deep + - exhaustive + description: |- + Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. + - `standard`: The default. Balances speed and depth, a good fit for most questions. + - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + default: standard + ResearchResponse: + type: object + properties: + output: + type: object + properties: + content: + oneOf: + - type: string + - type: object + description: The comprehensive response with inline citations. When content_type is "text", this is a Markdown string with numbered citations that reference the items in the sources array. When content_type is "object", this is a structured JSON object matching the requested output_schema. + content_type: + type: string + enum: + - text + - object + description: The format of the content field. + sources: + type: array + items: + type: object + properties: + url: + type: string + description: The URL of the source webpage. + title: + type: string + description: The title of the source webpage. + snippets: + type: array + items: + type: string + description: Relevant excerpts from the source page that were used in generating the answer. + required: + - url + description: A list of web sources used to generate the answer. + required: + - content + - content_type + - sources + description: The research output containing the answer and sources. + required: + - output + TaskResponse: + type: object + properties: + task_id: + type: string + format: uuid + description: Unique identifier for the task. + type: + type: string + description: Task type. + example: research + status: + type: string + enum: + - queued + - running + - completed + - failed + - cancelled + description: Current status of the task. + example: queued + stream_url: + type: string + description: URL to stream task events via SSE. + example: /v1/research/a1b2c3d4-0000-0000-0000-000000000000/stream + created_at: + type: string + format: date-time + description: When the task was created. + required: + - task_id + - type + - status + - stream_url + - created_at + TaskDetail: + type: object + properties: + id: + type: string + format: uuid + description: Unique identifier for the task. + task_type: + type: string + description: Task type. + example: research + status: + type: string + enum: + - queued + - running + - completed + - failed + - cancelled + description: Current status of the task. + created_at: + type: string + format: date-time + description: When the task was created. + updated_at: + type: string + format: date-time + description: When the task was last updated. + completed_at: + type: string + format: date-time + description: When the task completed, if applicable. + nullable: true + error: + type: string + description: Error message if the task failed. + nullable: true + input: + type: object + description: The original request input for the task. + nullable: true + result: + type: object + description: The task result when completed. For research tasks, this contains the ResearchResponse output. + nullable: true + required: + - id + - task_type + - status + - created_at + - updated_at + ResearchTaskStreamEvent: + type: object + properties: + id: + type: string + description: Sequence number of the SSE event. + event: + type: string + enum: + - connected + - response.done + - complete + - completed + - error + - failed + - cancelled + description: |- + The type of the SSE event. Most streams start with a `connected` event and then deliver terminal events `response.done`, `complete`, `error`, or `cancelled` from the worker. + If the SSE stream has aged out (after ~15 minutes) without any events flowing and the task is already in a terminal state, the server emits a synthetic event whose name is the task's status: one of `completed`, `failed`, or `cancelled`. Treat these synthetic event names the same as the corresponding worker-emitted names (`complete` ↔ `completed`, `error` ↔ `failed`, `cancelled` == `cancelled`). + data: + type: object + properties: + type: + type: string + description: The event type identifier. + task_id: + type: string + format: uuid + description: The task UUID. + status: + type: string + description: Current task status when the event was emitted. + data: + type: object + additionalProperties: true + description: Event-specific payload data. + error: + type: string + description: Error message if the event represents an error. + nullable: true + sequence: + type: integer + description: Event sequence number. + description: The event payload. Structure varies by event type. Common fields include type, task_id, status, data (event-specific), error, and sequence. + required: + - id + - event + - data + description: A server-sent event for a background research task stream. + FinanceResearchEffort: + type: string + enum: + - deep + - exhaustive + description: |- + Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. + default: deep + example: deep + responses: + SearchSuccess: + description: A JSON object containing unified search results from web and news sources + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + Unauthorized: + description: Unauthorized. Problems with API key. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + description: Error detail message. + examples: + missingApiKey: + summary: Missing API key + value: + detail: API key is required + invalidOrExpired: + summary: Invalid/expired API key + value: + detail: Invalid or expired API key + otherAuthParsing: + summary: Other auth parsing errors + value: + detail: + Forbidden: + description: Forbidden. API key lacks scope for this path. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + examples: + missingScopes: + summary: Missing required scopes + value: + detail: Missing required scopes + UnprocessableEntity: + description: Unprocessable Entity. Invalid request parameter combination. + content: + application/json: + schema: + type: object + properties: + error: + type: string + examples: + invalidParams: + summary: Invalid request parameters + value: + error: "invalid request parameter(s)" + InternalServerError: + description: Internal Server Error during authentication/authorization middleware. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + examples: + authFailure: + summary: Authentication failure + value: + detail: Internal authentication error + authorizationFailure: + summary: Authorization failure + value: + detail: Internal authorization error + parameters: + Query: + name: query + in: query + required: true + schema: + $ref: '#/components/schemas/SearchQuery' + Count: + name: count + in: query + required: false + schema: + $ref: '#/components/schemas/Count' + FreshnessParam: + name: freshness + in: query + required: false + schema: + $ref: '#/components/schemas/FreshnessValue' + Offset: + name: offset + in: query + required: false + schema: + $ref: '#/components/schemas/Offset' + CountryParam: + name: country + in: query + required: false + schema: + $ref: '#/components/schemas/Country' + LanguageParam: + name: language + in: query + required: false + schema: + $ref: '#/components/schemas/Language' + SafeSearchParam: + name: safesearch + in: query + required: false + schema: + $ref: '#/components/schemas/SafeSearch' + LiveCrawlParam: + name: livecrawl + in: query + required: false + schema: + $ref: '#/components/schemas/LiveCrawl' + LiveCrawlFormatsParam: + name: livecrawl_formats + in: query + required: false + style: form + explode: true + schema: + $ref: '#/components/schemas/LiveCrawlFormats' + IncludeDomainsParam: + name: include_domains + in: query + description: |- + A list of domains to restrict search results to. Only results from these domains will be returned. For large domain lists (up to 500), use POST with a JSON array instead. This is a strict allowlist — cannot be combined with `exclude_domains` (returns `422`). + + **Important:** Use a single comma-separated value (e.g. `include_domains=nytimes.com,bbc.com`). Repeated parameters (`include_domains=a.com&include_domains=b.com`) are not supported. + required: false + schema: + type: string + example: nytimes.com,bbc.com + ExcludeDomainsParam: + name: exclude_domains + in: query + description: |- + A list of domains to exclude from search results. Results from these domains will be filtered out. For large domain lists (up to 500), use POST with a JSON array instead. Cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `exclude_domains=spam-site.com,other-site.com`). Repeated parameters are not supported. + required: false + schema: + type: string + example: spam-site.com,other-site.com + BoostDomainsParam: + name: boost_domains + in: query + description: |- + A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `boost_domains=nytimes.com,wired.com`). Repeated parameters are not supported. + required: false + schema: + type: string + example: nytimes.com,wired.com + CrawlTimeout: + name: crawl_timeout + in: query + required: false + schema: + $ref: '#/components/schemas/CrawlTimeout' diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index 55180ec..858d084 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -1,31 +1,32 @@ -speakeasyVersion: 1.733.4 +speakeasyVersion: 1.789.1 sources: You.com API: sourceNamespace: you-com-search-api - sourceRevisionDigest: sha256:1d9828035b2b9ec387808b7167bd6f5d833492b9c95a7c236e736cacb24c2e2a - sourceBlobDigest: sha256:b436d65210d9571c37986fbf57326dabb68500ef8c1ac06ae8378768e6c8a254 + sourceRevisionDigest: sha256:1b718ccb4fb72a672b89e3703519f9f7593af632d689b8695991b76b0073193d + sourceBlobDigest: sha256:50bd60e232cd9939928c3a8d891b3c4e212b28600f933a1ec129aee2f9aff8d3 tags: - latest - - 1.0.0 + - 0.0.1 targets: you: source: You.com API sourceNamespace: you-com-search-api - sourceRevisionDigest: sha256:1d9828035b2b9ec387808b7167bd6f5d833492b9c95a7c236e736cacb24c2e2a - sourceBlobDigest: sha256:b436d65210d9571c37986fbf57326dabb68500ef8c1ac06ae8378768e6c8a254 + sourceRevisionDigest: sha256:1b718ccb4fb72a672b89e3703519f9f7593af632d689b8695991b76b0073193d + sourceBlobDigest: sha256:50bd60e232cd9939928c3a8d891b3c4e212b28600f933a1ec129aee2f9aff8d3 codeSamplesNamespace: you-com-search-api-code-samples - codeSamplesRevisionDigest: sha256:15160af083a36e6567c9dbd9a52eb1f963895eeb92bc129469febf19f65e74bb + codeSamplesRevisionDigest: sha256:b0a2333018ca37ecfcc9f824b023be519b5ee036f67882317ab8e80fbe460d3a workflow: workflowVersion: 1.0.0 speakeasyVersion: latest sources: You.com API: inputs: - - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_unified_agents.yaml - - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_search_v1.yaml - - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_contents.yaml - - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_research.yaml - - location: https://youdotcom-pr-11819.vercel.app/specs/openapi_base.yaml + - location: /Users/tyler/Workspace/youdotcom-frontend/public/specs/openapi_unified_agents.yaml + - location: /Users/tyler/Workspace/youdotcom-frontend/public/specs/openapi_search_v1.yaml + - location: /Users/tyler/Workspace/youdotcom-frontend/public/specs/openapi_contents.yaml + - location: /Users/tyler/Workspace/youdotcom-frontend/public/specs/openapi_base.yaml + - location: /Users/tyler/Workspace/youdotcom-frontend/public/specs/openapi_research.yaml + - location: /Users/tyler/Workspace/youdotcom-frontend/public/specs/openapi_finance_research.yaml overlays: - location: ./overlays/python_overlay.yaml output: .speakeasy/out.openapi.yaml diff --git a/.speakeasy/workflow.yaml b/.speakeasy/workflow.yaml index d93240b..bc2ad3b 100644 --- a/.speakeasy/workflow.yaml +++ b/.speakeasy/workflow.yaml @@ -8,6 +8,7 @@ sources: - location: https://you.com/specs/openapi_contents.yaml - location: https://you.com/specs/openapi_base.yaml - location: https://you.com/specs/openapi_research.yaml + - location: https://you.com/specs/openapi_finance_research.yaml overlays: - location: ./overlays/python_overlay.yaml output: .speakeasy/out.openapi.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a3a98..4a362ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,114 @@ 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). +## [2.4.0] - 2026-07-09 + +### Added + +- **Finance Research API**: New `you.finance_research()` method on the main `You` client. The Finance Research API searches a finance-optimized index — SEC filings, earnings transcripts, analyst coverage, market data, and financial news — instead of the open web. Use it for earnings analysis, due diligence, and market research. + +```python +from youdotcom import You +from youdotcom.models import FinanceResearchEffort + +you = You() +res = you.finance_research( + input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", + research_effort=FinanceResearchEffort.DEEP, +) +print(res.output.content) +for source in res.output.sources: + print(f" - {source.title or 'Untitled'}: {source.url}") +``` + +- **Research background mode**: New optional `background` parameter on `you.research()` and `you.research_async()`. When `True`, the request is queued as a task and a `TaskResponse` is returned immediately instead of blocking for the inline result. Use this for longer-running efforts (`deep`, `exhaustive`) that risk timeouts. + +```python +res = you.research( + input="Compare NVIDIA, AMD, and Intel profitability over 5 years", + research_effort=ResearchEffort.DEEP, + background=True, +) + +assert isinstance(res, TaskResponse) +# poll or stream +status = you.get_research_task(task_id=res.task_id) +``` + +- **Research task polling**: New `you.get_research_task(task_id)` and `get_research_task_async(task_id)` for polling background research tasks. Returns a `TaskDetail` with the current `status` and, when `completed`, the full `result` matching the synchronous `ResearchResponse` shape. + +- **Research SSE streaming**: New `you.stream_research_task(task_id)` and `stream_research_task_async(task_id)` for receiving real-time Server-Sent Events for a background task. Supports reconnection via `from_id=`. Event types: `connected`, `response.done` (terminal), `complete`, `error`, `cancelled`. + +```python +with you.stream_research_task(task_id=task_id) as stream: + for event in stream: + data = event.data + # handle connected / response.done / complete / error / cancelled + ... +``` + +- **Research `source_control` (beta)**: New optional `source_control` object on `you.research()` for constraining the research agent's web sources. Supports `include_domains`, `exclude_domains`, `boost_domains`, `freshness`, and `country`. `include_domains` and `exclude_domains` cannot be combined (returns `422`); `boost_domains` combines with `exclude_domains` but not `include_domains`. + +- **Research `output_schema` (beta)**: New optional `output_schema` object on `you.research()` for requesting structured JSON output in `output.content`. Response `content_type` becomes `"object"` and `output.content` is a structured dict matching the schema. Supported on `standard`, `deep`, and `exhaustive` effort levels (sending it with `lite` returns `422`). + +- **Search API `boost_domains`**: New optional parameter on `you.search_post()` and on the underlying `you.search.unified()` (also accessible via `GET /v1/search`). Boost (but don't restrict) results from specified domains. Up to 500 domains per request. Cannot be combined with `include_domains`. + +- **Contents API `max_age`**: New optional `max_age` parameter (integer seconds, ≥0, nullable) for controlling cache freshness. When set, cached content older than the threshold is ignored and the page is re-fetched. Default `null` (no age limit). + +### Changed + +- **`Research` API response is now `Union[ResearchResponse, TaskResponse]`**: The `POST /v1/research` 200 response is now a `oneOf` between inline `ResearchResponse` and the new `TaskResponse` returned when `background=True`. Update code that asserts on `isinstance(res, ResearchResponse)` to handle both shapes (or use type narrowing based on whether you passed `background=True`). + +- **`Research.output.content` is now `Union[str, object]`**: When an `output_schema` is supplied, the server returns a structured JSON object and `content_type` becomes `"object"`. **Caveat (2.4.0)**: the generated `Content` model currently declares no fields and uses pydantic's default `extra="ignore"`, so the structured payload is dropped at unmarshal time — `output.content` comes back as an empty `Content()` instance, not the dict. To retrieve the structured object, re-issue the same call synchronously with `background=False` (see `MIGRATION.md` for the full workaround). An overlay fix (`additionalProperties: true`) is staged in `overlays/python_overlay.yaml` for the next regeneration cycle, after which `output.content` will round-trip the dict directly. Check `output.content_type` to deserialise correctly: `text` → str, `object` → dict. + +- **New `FinanceResearchEffort` enum**: The Finance Research API has its own effort enum (`DEEP`, `EXHAUSTIVE`) distinct from the Research API's `ResearchEffort`. Both have clean names — `ResearchEffort` is unchanged from 2.3.x. + +- **Livecrawl formats parameter now requires a list**: The `livecrawl_formats` parameter is now strictly typed as `Optional[List[LiveCrawlFormats]]`. Passing a single enum value (which worked in prior versions) now raises a validation error. Wrap the value in a list: + +```python +# Before (2.3.x) +you.search.unified(query="...", livecrawl_formats=LiveCrawlFormats.MARKDOWN) + +# After (2.4.0) +you.search.unified(query="...", livecrawl_formats=[LiveCrawlFormats.MARKDOWN]) +``` + +- **Consolidated error classes**: The shared `422`, `401`, `403` response shapes for Research and Search endpoints are now exposed as consolidated `UnprocessableEntityResponseError`, `UnauthorizedResponseError`, and `ForbiddenResponseError` instead of per-endpoint `SearchUnprocessableEntityError` / `ResearchUnprocessableEntityError` etc. Per-endpoint typed errors (`ResearchUnauthorizedError`, `FinanceResearchUnprocessableEntityError`, etc.) are still available as the primary raise target, but the bare-from-spec names like `UnprocessableEntityError` and `SearchForbiddenError` are gone. Catch on either the per-endpoint class or `YouDefaultError` for backward compatibility. + +- **Environment variable renamed to `YDC_API_KEY`**: The SDK now reads the `YDC_API_KEY` environment variable for API key authentication (canonical per `you.com/docs`). The previous `YOU_API_KEY_AUTH` is still accepted as a fallback for 2.3.x users upgrading without code changes. Set `YDC_API_KEY` in your environment and the SDK will pick it up automatically: + +```bash +# Before (2.3.x) +export YOU_API_KEY_AUTH="your-api-key" + +# After (2.4.0) — preferred +export YDC_API_KEY="your-api-key" +# YOU_API_KEY_AUTH still works as a fallback +``` + +### Notes + +- The `unresearched` `ulow` effort level remains internal and is intentionally NOT exposed in the SDK — it is consolidated as internal routing on the server. +- `you.finance_research()` deliberately does not support `source_control` or `output_schema`. The Finance Research API runs against a finance-optimized index and returns Markdown-formatted answers only. +- Background-mode + SSE streaming endpoints are considered ahead-of-docs and may receive minor surface changes before being documented at `docs.you.com`. The Python SDK contract matches the server implementation (`background`, `GET /v1/research/{task_id}`, `GET /v1/research/{task_id}/stream`) as of this release. +- **`pydantic` upper bound pinned to `<2.13`**: Defensive pin to avoid potential breaking changes in pydantic 2.13+ (the SDK relies on `extra="ignore"` default behavior, `model_dump()`, and `ConfigDict` patterns that could shift across minor versions). Will be re-evaluated as pydantic stabilizes. + +### Hand-maintained additions (not regenerated) + +These live in `src/youdotcom/research_helpers.py`, `src/youdotcom/_hooks/registration.py`, and parts of `src/youdotcom/utils/security.py` and are NOT regenerated by Speakeasy — future SDK regens will overwrite them. The next release MUST re-apply the hand-edits below (or move them into the overlay / `x-speakeasy-env-var` extension before regen) so they survive regeneration: + +- **`security.py` env-var precedence**: `get_security_from_env` reads `YDC_API_KEY` first and falls back to `YOU_API_KEY_AUTH` for backward compatibility with the 2.3.x env-var name. Covered by `tests/test_security_env.py`. Future regens that drop the fallback will lose `2.3.x` users — re-apply the two-line `or` chain after regen, or move the precedence into the Speakeasy overlay. + +- **`research_helpers` module**: New `youdotcom.research_helpers` with the following public helpers: + - `research_background(client, **kwargs)` / `research_background_async`: Submit research with `background=True` and return a typed `TaskResponse` directly (no need to narrow `Union[ResearchResponse, TaskResponse]`). + - `poll_research_task(client, task_id, *, interval_s, timeout_s)` / `poll_research_task_async`: Poll `GET /v1/research/{task_id}` until status reaches a terminal state (`completed`, `failed`, `cancelled`); raises `RuntimeError`/`TimeoutError` accordingly. + - `research_and_wait(client, *, mode, **kwargs)` / `research_and_wait_async`: Submit + wait (poll or stream) until done, returning the final `TaskDetail`. Note: today's SDK unmarshals the `Result` model with `extra=ignore`, so the inline `ResearchResponse` is not typed through this path — `detail.result.model_dump()` returns an empty dict because the typed model has no schema-declared fields and pydantic drops the `output` data. The supported workaround is to issue a synchronous `client.research(..., background=False)` call with the same input once the task reaches `completed`. + - `stream_research_events_raw(client, task_id)` / `stream_research_events_raw_async`: SSE iterator that yields `RawStreamEvent(id, event, data, retry)` and accepts event names outside the documented enum (`connected`/`response.done`/`complete`/`error`/`cancelled`). Use this in place of `client.stream_research_task(...)` when the server may emit intermediate workflow events (`research.searching`, etc.). + +- **`YDCUserAgentOverrideHook` honors custom `user_agent`**: Previously the hook unconditionally rewrote `User-Agent` to `youdotcom-python-sdk/{sdk_version}`. Now it detects when `sdk_configuration.user_agent` has been overridden away from the speakeasy default (`speakeasy-sdk/python ...`) and passes the custom value through. Integrations (langchain-youdotcom, youdotcom-temporal, n8n-nodes-youdotcom) can now simply set `client.sdk_configuration.user_agent = "/"` after construction instead of swapping hooks. + +--- + ## [2.3.0] - 2026-02-27 ### Added diff --git a/MIGRATION.md b/MIGRATION.md index 519d851..2f0e31c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,181 @@ # Migration Guide -## 1.x → 2.3.0 (Latest) +## 2.3.0 → 2.4.0 (Latest) + +This guide covers breaking changes introduced in 2.4.0. If you are upgrading from 1.x or 2.0, also read the [1.x → 2.0](#1x-to-20) section below. + +### Breaking Changes in 2.4.0 + +#### New `FinanceResearchEffort` enum + +The Finance Research API has its own effort enum (`DEEP`, `EXHAUSTIVE`) distinct from the Research API's `ResearchEffort` enum (which is unchanged): + +```python +# Research API (unchanged from 2.3.x) +from youdotcom.models import ResearchEffort +you.research(input="...", research_effort=ResearchEffort.DEEP) + +# New in 2.4.0: Finance Research API +from youdotcom.models import FinanceResearchEffort +you.finance_research(input="...", research_effort=FinanceResearchEffort.DEEP) +``` + +`ResearchEffort` keeps the name `ResearchEffort` and values `LITE`, `STANDARD`, `DEEP`, `EXHAUSTIVE`. No migration is required — the OpenAPI spec was promoted to a named schema so the SDK preserves the clean name. + +#### `livecrawl_formats` now requires a list + +`livecrawl_formats` on the Search API is now strictly typed as `Optional[List[LiveCrawlFormats]]`. Passing a single enum value (silently coerced in earlier versions) raises a `ValidationError` at request time. Wrap the value in a list: + +```python +# Before (2.3.x): single value was accepted +you.search.unified( + query="...", + livecrawl=LiveCrawl.WEB, + livecrawl_formats=LiveCrawlFormats.MARKDOWN, +) + +# After (2.4.0): must be a list +you.search.unified( + query="...", + livecrawl=LiveCrawl.WEB, + livecrawl_formats=[LiveCrawlFormats.MARKDOWN], +) +``` + +If you request multiple formats, the list form is the only available form: + +```python +you.search.unified( + query="...", + livecrawl=LiveCrawl.WEB, + livecrawl_formats=[LiveCrawlFormats.HTML, LiveCrawlFormats.MARKDOWN], +) +``` + +#### Research response is now `Union[ResearchResponse, TaskResponse]` + +`you.research()` now returns either `ResearchResponse` (the inline answer, default) or `TaskResponse` (a task handle) depending on the `background` parameter. Code that asserts `isinstance(res, ResearchResponse)` still works for synchronous research, but be aware that: + +```python +# Synchronous (unchanged behaviour) +res = you.research(input="...", research_effort=ResearchEffort.STANDARD) +assert isinstance(res, ResearchResponse) # still True + +# New: background-mode returns a TaskResponse +res = you.research( + input="...", + research_effort=ResearchEffort.DEEP, + background=True, +) +assert isinstance(res, TaskResponse) +``` + +If you use `You` as a `TypedDict`-style client and only pass synchronous keyword arguments, this change is transparent. + +#### Research `output.content` is now `Union[str, object]` + +`output.content` is now `Union[str, object]` instead of always `str`. Plain research responses still return a Markdown `str` (with `content_type="text"`). Only when you supply `output_schema=...` does the SDK deserialize `output.content` as a structured JSON object matching your schema (with `content_type="object"`). + +```python +res = you.research( + input="Are Acme Logistics DE and Acme Logistics NJ the same entity?", + output_schema={ + "type": "object", + "properties": { + "same_entity": {"type": "boolean"}, + "confidence": {"type": "number"}, + "evidence": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["same_entity", "confidence", "evidence"], + }, +) +assert res.output.content_type.value == "object" +# Caveat (2.4.0): the typed `Content` model declares no fields and uses +# pydantic's default `extra="ignore"`, so the JSON payload returned by the +# server is dropped at unmarshal time. `res.output.content` is an empty +# `Content()` instance rather than the structured dict. To get a typed +# `ResearchResponse` with the structured object, re-issue the same call +# synchronously with `background=False`: +typed = you.research( + input="Are Acme Logistics DE and Acme Logistics NJ the same entity?", + output_schema={...}, # same schema as above +) +print(typed.output.content if isinstance(typed.output.content, dict) else "...") +``` + +Code that does `res.output.content.lower()` or similar string-only operations will still work for typical text responses (the value remains a `str`), but if you opt into `output_schema` you must branch on `content_type` before calling string methods. + +#### Environment variable renamed to `YDC_API_KEY` + +The SDK now reads `YDC_API_KEY` (canonical per `you.com/docs`) instead of `YOU_API_KEY_AUTH` for API key authentication. `YOU_API_KEY_AUTH` is still accepted as a fallback, so existing 2.3.x users do not need to change anything immediately. Update your environment to use the canonical name when convenient: + +```bash +# Before (2.3.x) +export YOU_API_KEY_AUTH="your-api-key" + +# After (2.4.0) — preferred +export YDC_API_KEY="your-api-key" +# YOU_API_KEY_AUTH still works as a fallback +``` + +### Optional Migrations Worth Adopting + +#### Use background mode for heavy research efforts + +For `ResearchEffort.DEEP` and `EXHAUSTIVE` calls, prefer background mode + polling or streaming to avoid client-side timeouts: + +```python +# Recommended for long-running research +res = you.research( + input="deep, multi-source question...", + research_effort=ResearchEffort.EXHAUSTIVE, + background=True, +) + +while True: + status = you.get_research_task(task_id=res.task_id) + if status.status.value == "completed": + break + time.sleep(5) +``` + +#### Adopt new typed error names + +The catch surface for Research has shifted from bare-class names to per-endpoint classes: + +```python +# Before (2.3.x) +from youdotcom.errors import UnprocessableEntityError + +# After (2.4.0): prefer per-endpoint +from youdotcom.errors import ( + ResearchUnprocessableEntityError, # research-specific + FinanceResearchUnprocessableEntityError, # new + YouDefaultError, # safety net +) + +try: + you.research(input="") +except ResearchUnprocessableEntityError as e: + ... +except YouDefaultError as e: + ... +``` + +The bare `UnprocessableEntityError` / `SearchUnauthorizedError` / `SearchForbiddenError` names are gone. Code that catches on `YouDefaultError` or on `(SomeError, YouDefaultError)` tuples is unaffected. + +### New APIs to Try + +- `you.finance_research(input=..., research_effort=FinanceResearchEffort.DEEP)` — finance-optimized index. +- `you.research(..., background=True)` + `you.get_research_task(task_id)` / `you.stream_research_task(task_id)` — long-running research with poll/stream. +- `you.research(..., source_control={...})` — restrict / boost / exclude domains or filter by recency or country. +- `you.research(..., output_schema={...})` — structured JSON output. +- `you.search_post(..., boost_domains=[...])` (POST takes a list) or `you.search.unified(..., boost_domains="nytimes.com,wired.com")` (GET takes a single comma-separated string) — boost (but don't restrict) matching domains in ranking. +- `you.contents.generate(..., max_age=86400)` — require cached content younger than 24 hours. + +--- + +## 1.x → 2.3.0 This guide covers breaking changes introduced in 2.3.0. If you are upgrading from 1.x, also read the [1.x → 2.0](#1x-to-20) section below. diff --git a/README.md b/README.md index ae045c0..30da687 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,14 @@ The official developer-friendly & type-safe Python SDK specifically designed to You.com API: Unified API for Express, Advanced, and Custom Agents from You.com Get the best search results from web and news sources Returns the HTML or Markdown of a target webpage -Multi-step reasoning with comprehensive research capabilities Comprehensive API for You.com services: - **Agents API**: Execute queries using Express, Advanced, and Custom AI agents - **Research API**: In-depth, multi-step research with citations and sources +- **Finance Research API**: Finance-focused multi-step research with citations and sources - **Search API**: Get search results from web and news sources - **Contents API**: Retrieve and process web page content +Multi-step reasoning with comprehensive research capabilities +Finance-focused multi-step research with competitive accuracy at same price points and latencies as the Research API @@ -136,10 +138,16 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) # Handle response print(res) @@ -158,10 +166,16 @@ from youdotcom import You, models async def main(): async with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = await you.research_async(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) + res = await you.search_post_async(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) # Handle response print(res) @@ -180,7 +194,7 @@ This SDK supports the following security scheme globally: | Name | Type | Scheme | Environment Variable | | -------------- | ------ | ------- | -------------------- | -| `api_key_auth` | apiKey | API key | `YOU_API_KEY_AUTH` | +| `api_key_auth` | apiKey | API key | `YDC_API_KEY` | To authenticate with the API the `api_key_auth` parameter must be set when initializing the SDK client instance. For example: ```python @@ -189,10 +203,16 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) # Handle response print(res) @@ -208,7 +228,11 @@ with You( ### [You SDK](docs/sdks/you/README.md) +* [search_post](docs/sdks/you/README.md#search_post) - Returns a list of unified search results from web and news sources * [research](docs/sdks/you/README.md#research) - Returns comprehensive research-grade answers with multi-step reasoning +* [get_research_task](docs/sdks/you/README.md#get_research_task) - Get the status of a background research task +* [stream_research_task](docs/sdks/you/README.md#stream_research_task) - Stream updates for a background research task +* [finance_research](docs/sdks/you/README.md#finance_research) - Returns comprehensive finance-grade research answers with multi-step reasoning ### [Agents.Runs](docs/sdks/runs/README.md) @@ -255,7 +279,7 @@ from youdotcom.utils import eventstreaming with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: response = you.agents.runs.create(request=ExpressAgentRunsRequest( @@ -325,14 +349,17 @@ from youdotcom.utils import BackoffStrategy, RetryConfig with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research( - input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", - research_effort=models.ResearchEffort.LITE, - retries=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False), - ) + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10, + retries=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False)) # Handle response print(res) @@ -348,10 +375,16 @@ from youdotcom.utils import BackoffStrategy, RetryConfig with You( retry_config=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False), - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) # Handle response print(res) @@ -380,12 +413,18 @@ from youdotcom import You, errors, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = None try: - res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) # Handle response print(res) @@ -400,7 +439,7 @@ with You( print(e.raw_response) # Depending on the method different errors may be thrown - if isinstance(e, errors.ResearchUnauthorizedError): + if isinstance(e, errors.UnauthorizedResponseError): print(e.data.detail) # Optional[str] ``` @@ -408,7 +447,7 @@ with You( **Primary error:** * [`YouError`](./src/youdotcom/errors/youerror.py): The base class for HTTP error responses. -
Less common errors (18) +
Less common errors (31)
@@ -419,19 +458,32 @@ with You( **Inherit from [`YouError`](./src/youdotcom/errors/youerror.py)**: -* [`AgentRuns400ResponseError`](./src/youdotcom/errors/agentruns400responseerror.py): The message returned by the error. Status code `400`. Applicable to 1 of 4 methods.* -* [`ResearchUnauthorizedError`](./src/youdotcom/errors/researchunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 4 methods.* -* [`SearchUnauthorizedError`](./src/youdotcom/errors/searchunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 4 methods.* -* [`ContentsUnauthorizedError`](./src/youdotcom/errors/contentsunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 4 methods.* -* [`AgentRuns401ResponseError`](./src/youdotcom/errors/agentruns401responseerror.py): The message returned by the error. Status code `401`. Applicable to 1 of 4 methods.* -* [`ResearchForbiddenError`](./src/youdotcom/errors/researchforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 4 methods.* -* [`SearchForbiddenError`](./src/youdotcom/errors/searchforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 4 methods.* -* [`ContentsForbiddenError`](./src/youdotcom/errors/contentsforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 4 methods.* -* [`UnprocessableEntityError`](./src/youdotcom/errors/unprocessableentityerror.py): Unprocessable Entity. Request validation failed. Status code `422`. Applicable to 1 of 4 methods.* -* [`AgentRuns422ResponseError`](./src/youdotcom/errors/agentruns422responseerror.py): Unprocessable Entity - Invalid request data. Status code `422`. Applicable to 1 of 4 methods.* -* [`ResearchInternalServerError`](./src/youdotcom/errors/researchinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 4 methods.* -* [`SearchInternalServerError`](./src/youdotcom/errors/searchinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 4 methods.* -* [`ContentsInternalServerError`](./src/youdotcom/errors/contentsinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 4 methods.* +* [`UnauthorizedResponseError`](./src/youdotcom/errors/unauthorizedresponseerror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 2 of 8 methods.* +* [`ForbiddenResponseError`](./src/youdotcom/errors/forbiddenresponseerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 2 of 8 methods.* +* [`UnprocessableEntityResponseError`](./src/youdotcom/errors/unprocessableentityresponseerror.py): Unprocessable Entity. Invalid request parameter combination. Status code `422`. Applicable to 2 of 8 methods.* +* [`InternalServerErrorResponse`](./src/youdotcom/errors/internalservererrorresponse.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 2 of 8 methods.* +* [`AgentRuns400ResponseError`](./src/youdotcom/errors/agentruns400responseerror.py): The message returned by the error. Status code `400`. Applicable to 1 of 8 methods.* +* [`ResearchUnauthorizedError`](./src/youdotcom/errors/researchunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 8 methods.* +* [`GetResearchTaskUnauthorizedError`](./src/youdotcom/errors/getresearchtaskunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 8 methods.* +* [`StreamResearchTaskUnauthorizedError`](./src/youdotcom/errors/streamresearchtaskunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 8 methods.* +* [`FinanceResearchUnauthorizedError`](./src/youdotcom/errors/financeresearchunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 8 methods.* +* [`ContentsUnauthorizedError`](./src/youdotcom/errors/contentsunauthorizederror.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 8 methods.* +* [`AgentRuns401ResponseError`](./src/youdotcom/errors/agentruns401responseerror.py): The message returned by the error. Status code `401`. Applicable to 1 of 8 methods.* +* [`ResearchForbiddenError`](./src/youdotcom/errors/researchforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 8 methods.* +* [`GetResearchTaskForbiddenError`](./src/youdotcom/errors/getresearchtaskforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 8 methods.* +* [`StreamResearchTaskForbiddenError`](./src/youdotcom/errors/streamresearchtaskforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 8 methods.* +* [`FinanceResearchForbiddenError`](./src/youdotcom/errors/financeresearchforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 8 methods.* +* [`ContentsForbiddenError`](./src/youdotcom/errors/contentsforbiddenerror.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 8 methods.* +* [`GetResearchTaskNotFoundError`](./src/youdotcom/errors/getresearchtasknotfounderror.py): Task not found or not authorized. Status code `404`. Applicable to 1 of 8 methods.* +* [`StreamResearchTaskNotFoundError`](./src/youdotcom/errors/streamresearchtasknotfounderror.py): Task not found or not authorized. Status code `404`. Applicable to 1 of 8 methods.* +* [`ResearchUnprocessableEntityError`](./src/youdotcom/errors/researchunprocessableentityerror.py): Unprocessable Entity. Request validation failed. Status code `422`. Applicable to 1 of 8 methods.* +* [`FinanceResearchUnprocessableEntityError`](./src/youdotcom/errors/financeresearchunprocessableentityerror.py): Unprocessable Entity. Request validation failed. Status code `422`. Applicable to 1 of 8 methods.* +* [`AgentRuns422ResponseError`](./src/youdotcom/errors/agentruns422responseerror.py): Unprocessable Entity - Invalid request data. Status code `422`. Applicable to 1 of 8 methods.* +* [`ResearchInternalServerError`](./src/youdotcom/errors/researchinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 8 methods.* +* [`GetResearchTaskInternalServerError`](./src/youdotcom/errors/getresearchtaskinternalservererror.py): Internal Server Error. Status code `500`. Applicable to 1 of 8 methods.* +* [`StreamResearchTaskInternalServerError`](./src/youdotcom/errors/streamresearchtaskinternalservererror.py): Internal Server Error. Status code `500`. Applicable to 1 of 8 methods.* +* [`FinanceResearchInternalServerError`](./src/youdotcom/errors/financeresearchinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 8 methods.* +* [`ContentsInternalServerError`](./src/youdotcom/errors/contentsinternalservererror.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 8 methods.* * [`ResponseValidationError`](./src/youdotcom/errors/responsevalidationerror.py): Type mismatch between the response data and the expected Pydantic model. Provides access to the Pydantic validation error via the `cause` attribute.
@@ -452,10 +504,10 @@ from youdotcom import You, models with You( server_url="https://api.you.com", - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) + res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE, background=False) # Handle response print(res) @@ -471,10 +523,16 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.search.unified(query="Your query", count=10, language=models.Language.EN, server_url="https://ydc-index.io") + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10, server_url="https://ydc-index.io") # Handle response print(res) @@ -576,7 +634,7 @@ from youdotcom import You def main(): with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: # Rest of application here... @@ -585,7 +643,7 @@ def main(): async def amain(): async with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: # Rest of application here... ``` diff --git a/USAGE.md b/USAGE.md index 9f08d21..37e0e9d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -6,10 +6,16 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) # Handle response print(res) @@ -28,10 +34,16 @@ from youdotcom import You, models async def main(): async with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = await you.research_async(input="Which global cities improved air quality the most over the past 10 years, and what measurable actions contributed?", research_effort=models.ResearchEffort.LITE) + res = await you.search_post_async(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) # Handle response print(res) diff --git a/docs/errors/financeresearchforbiddenerror.md b/docs/errors/financeresearchforbiddenerror.md new file mode 100644 index 0000000..9e6c3a9 --- /dev/null +++ b/docs/errors/financeresearchforbiddenerror.md @@ -0,0 +1,10 @@ +# FinanceResearchForbiddenError + +Forbidden. API key lacks scope for this path. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/financeresearchinternalservererror.md b/docs/errors/financeresearchinternalservererror.md new file mode 100644 index 0000000..0556209 --- /dev/null +++ b/docs/errors/financeresearchinternalservererror.md @@ -0,0 +1,10 @@ +# FinanceResearchInternalServerError + +Internal Server Error during authentication/authorization middleware. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/financeresearchunauthorizederror.md b/docs/errors/financeresearchunauthorizederror.md new file mode 100644 index 0000000..2cf8870 --- /dev/null +++ b/docs/errors/financeresearchunauthorizederror.md @@ -0,0 +1,10 @@ +# FinanceResearchUnauthorizedError + +Unauthorized. Problems with API key. + + +## Fields + +| Field | Type | Required | Description | +| --------------------- | --------------------- | --------------------- | --------------------- | +| `detail` | *Optional[str]* | :heavy_minus_sign: | Error detail message. | \ No newline at end of file diff --git a/docs/errors/financeresearchunprocessableentityerror.md b/docs/errors/financeresearchunprocessableentityerror.md new file mode 100644 index 0000000..470b417 --- /dev/null +++ b/docs/errors/financeresearchunprocessableentityerror.md @@ -0,0 +1,10 @@ +# FinanceResearchUnprocessableEntityError + +Unprocessable Entity. Request validation failed. + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | +| `detail` | List[[models.FinanceResearchDetail](../models/financeresearchdetail.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/searchforbiddenerror.md b/docs/errors/forbiddenresponseerror.md similarity index 92% rename from docs/errors/searchforbiddenerror.md rename to docs/errors/forbiddenresponseerror.md index 3a7b4fc..5d49673 100644 --- a/docs/errors/searchforbiddenerror.md +++ b/docs/errors/forbiddenresponseerror.md @@ -1,4 +1,4 @@ -# SearchForbiddenError +# ForbiddenResponseError Forbidden. API key lacks scope for this path. diff --git a/docs/errors/getresearchtaskforbiddenerror.md b/docs/errors/getresearchtaskforbiddenerror.md new file mode 100644 index 0000000..cc633bb --- /dev/null +++ b/docs/errors/getresearchtaskforbiddenerror.md @@ -0,0 +1,10 @@ +# GetResearchTaskForbiddenError + +Forbidden. API key lacks scope for this path. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/getresearchtaskinternalservererror.md b/docs/errors/getresearchtaskinternalservererror.md new file mode 100644 index 0000000..ebed213 --- /dev/null +++ b/docs/errors/getresearchtaskinternalservererror.md @@ -0,0 +1,10 @@ +# GetResearchTaskInternalServerError + +Internal Server Error. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/getresearchtasknotfounderror.md b/docs/errors/getresearchtasknotfounderror.md new file mode 100644 index 0000000..cbb274c --- /dev/null +++ b/docs/errors/getresearchtasknotfounderror.md @@ -0,0 +1,10 @@ +# GetResearchTaskNotFoundError + +Task not found or not authorized. + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | Task not found | \ No newline at end of file diff --git a/docs/errors/getresearchtaskunauthorizederror.md b/docs/errors/getresearchtaskunauthorizederror.md new file mode 100644 index 0000000..5ed32fa --- /dev/null +++ b/docs/errors/getresearchtaskunauthorizederror.md @@ -0,0 +1,10 @@ +# GetResearchTaskUnauthorizedError + +Unauthorized. Problems with API key. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/searchinternalservererror.md b/docs/errors/internalservererrorresponse.md similarity index 91% rename from docs/errors/searchinternalservererror.md rename to docs/errors/internalservererrorresponse.md index 28d41b1..b99d7b0 100644 --- a/docs/errors/searchinternalservererror.md +++ b/docs/errors/internalservererrorresponse.md @@ -1,4 +1,4 @@ -# SearchInternalServerError +# InternalServerErrorResponse Internal Server Error during authentication/authorization middleware. diff --git a/docs/errors/unprocessableentityerror.md b/docs/errors/researchunprocessableentityerror.md similarity index 95% rename from docs/errors/unprocessableentityerror.md rename to docs/errors/researchunprocessableentityerror.md index 5abff96..603a7fd 100644 --- a/docs/errors/unprocessableentityerror.md +++ b/docs/errors/researchunprocessableentityerror.md @@ -1,4 +1,4 @@ -# UnprocessableEntityError +# ResearchUnprocessableEntityError Unprocessable Entity. Request validation failed. diff --git a/docs/errors/streamresearchtaskforbiddenerror.md b/docs/errors/streamresearchtaskforbiddenerror.md new file mode 100644 index 0000000..f13d63a --- /dev/null +++ b/docs/errors/streamresearchtaskforbiddenerror.md @@ -0,0 +1,10 @@ +# StreamResearchTaskForbiddenError + +Forbidden. API key lacks scope for this path. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/streamresearchtaskinternalservererror.md b/docs/errors/streamresearchtaskinternalservererror.md new file mode 100644 index 0000000..e7d0083 --- /dev/null +++ b/docs/errors/streamresearchtaskinternalservererror.md @@ -0,0 +1,10 @@ +# StreamResearchTaskInternalServerError + +Internal Server Error. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/streamresearchtasknotfounderror.md b/docs/errors/streamresearchtasknotfounderror.md new file mode 100644 index 0000000..8d8ff6b --- /dev/null +++ b/docs/errors/streamresearchtasknotfounderror.md @@ -0,0 +1,10 @@ +# StreamResearchTaskNotFoundError + +Task not found or not authorized. + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | Task not found | \ No newline at end of file diff --git a/docs/errors/streamresearchtaskunauthorizederror.md b/docs/errors/streamresearchtaskunauthorizederror.md new file mode 100644 index 0000000..86e9ae7 --- /dev/null +++ b/docs/errors/streamresearchtaskunauthorizederror.md @@ -0,0 +1,10 @@ +# StreamResearchTaskUnauthorizedError + +Unauthorized. Problems with API key. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/searchunauthorizederror.md b/docs/errors/unauthorizedresponseerror.md similarity index 92% rename from docs/errors/searchunauthorizederror.md rename to docs/errors/unauthorizedresponseerror.md index f799351..fe02660 100644 --- a/docs/errors/searchunauthorizederror.md +++ b/docs/errors/unauthorizedresponseerror.md @@ -1,4 +1,4 @@ -# SearchUnauthorizedError +# UnauthorizedResponseError Unauthorized. Problems with API key. diff --git a/docs/errors/unprocessableentityresponseerror.md b/docs/errors/unprocessableentityresponseerror.md new file mode 100644 index 0000000..5d44499 --- /dev/null +++ b/docs/errors/unprocessableentityresponseerror.md @@ -0,0 +1,10 @@ +# UnprocessableEntityResponseError + +Unprocessable Entity. Invalid request parameter combination. + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `error` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/agentrunsbatchresponse.md b/docs/models/agentrunsbatchresponse.md index 5922b79..7c345a6 100644 --- a/docs/models/agentrunsbatchresponse.md +++ b/docs/models/agentrunsbatchresponse.md @@ -3,9 +3,9 @@ ## Fields -| Field | Type | Required | Description | Example | -| ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| `agent` | *str* | :heavy_check_mark: | The id of the agent populated in the request. | express | -| `mode` | *Optional[str]* | :heavy_minus_sign: | The mode of the agent | express | -| `input` | List[[models.Input1](../models/input1.md)] | :heavy_check_mark: | The users access role and question you asked the agent | | -| `output` | List[[models.AgentRunsResponseOutput](../models/agentrunsresponseoutput.md)] | :heavy_check_mark: | Array of response outputs from the agent | | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| `agent` | *str* | :heavy_check_mark: | The id of the agent populated in the request. | express | +| `mode` | *Optional[str]* | :heavy_minus_sign: | The mode of the agent | express | +| `input` | List[[models.AgentRunsBatchResponseInput](../models/agentrunsbatchresponseinput.md)] | :heavy_check_mark: | The users access role and question you asked the agent | | +| `output` | List[[models.AgentRunsResponseOutput](../models/agentrunsresponseoutput.md)] | :heavy_check_mark: | Array of response outputs from the agent | | \ No newline at end of file diff --git a/docs/models/input1.md b/docs/models/agentrunsbatchresponseinput.md similarity index 97% rename from docs/models/input1.md rename to docs/models/agentrunsbatchresponseinput.md index 8b6df1b..b15c573 100644 --- a/docs/models/input1.md +++ b/docs/models/agentrunsbatchresponseinput.md @@ -1,4 +1,4 @@ -# Input1 +# AgentRunsBatchResponseInput ## Fields diff --git a/docs/models/agentrunsresponsewebsearchresult.md b/docs/models/agentrunsresponsewebsearchresult.md index 6bdcf11..95bff3e 100644 --- a/docs/models/agentrunsresponsewebsearchresult.md +++ b/docs/models/agentrunsresponsewebsearchresult.md @@ -9,7 +9,7 @@ The text response of the agent. This field only returns when the type is `web_se | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `source_type` | *Literal["web_search"]* | :heavy_check_mark: | The type of content the agent can return outside a text response | web_search | | `citation_uri` | *str* | :heavy_check_mark: | The web search result the agent returned along in its response | https://www.foodnetwork.com/recipes/photos/30-minute-dinner-recipes | -| `provider` | *Optional[str]* | :heavy_minus_sign: | This is currently unused | | +| `provider` | *Optional[str]* | :heavy_minus_sign: | This is currently unused | null | | `title` | *str* | :heavy_check_mark: | The title of the web site returned under url | 103 Easy 30-Minute Dinner Recipes That Will Save Your Weeknights | | `snippet` | *str* | :heavy_check_mark: | A textual portion of the web site returned under url | Apr 11, 2025 ... These quick dinner ideas will help you get a meal on the table in half an hour or less. ... It's a simple recipe ready in under half an hour with ... | | `thumbnail_url` | *Optional[str]* | :heavy_minus_sign: | The thumbnail image of the url | https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQJTNucjGK8ZqfurwAmyuyhQ-7n7AVZHoJwUqfsqRYuCqlIpMwepNDEE_M&s | diff --git a/docs/models/content.md b/docs/models/content.md new file mode 100644 index 0000000..88bdbee --- /dev/null +++ b/docs/models/content.md @@ -0,0 +1,7 @@ +# Content + + +## Fields + +| Field | Type | Required | Description | +| ----------- | ----------- | ----------- | ----------- | \ No newline at end of file diff --git a/docs/models/contentsformats.md b/docs/models/contentsformats.md index 1944732..8510c9a 100644 --- a/docs/models/contentsformats.md +++ b/docs/models/contentsformats.md @@ -1,5 +1,13 @@ # ContentsFormats +## Example Usage + +```python +from youdotcom.models import ContentsFormats + +value = ContentsFormats.HTML +``` + ## Values diff --git a/docs/models/contentsrequest.md b/docs/models/contentsrequest.md index fec30b0..847c157 100644 --- a/docs/models/contentsrequest.md +++ b/docs/models/contentsrequest.md @@ -3,8 +3,9 @@ ## Fields -| Field | Type | Required | Description | Example | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `urls` | List[*str*] | :heavy_minus_sign: | Array of URLs to fetch the contents from. | | -| `formats` | List[[models.ContentsFormats](../models/contentsformats.md)] | :heavy_minus_sign: | Array of content formats to return. All included formats are returned in the response. Include "metadata" to get JSON-LD and OpenGraph information, if available. | [
"html",
"markdown"
] | -| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. | 10 | \ No newline at end of file +| Field | Type | Required | Description | Example | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `urls` | List[*str*] | :heavy_minus_sign: | Array of URLs to fetch the contents from. | | +| `formats` | List[[models.ContentsFormats](../models/contentsformats.md)] | :heavy_minus_sign: | Array of content formats to return. All included formats are returned in the response. Include "metadata" to get JSON-LD and OpenGraph information, if available. | [
"html",
"markdown"
] | +| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. | 10 | +| `max_age` | *OptionalNullable[int]* | :heavy_minus_sign: | Maximum allowed age of cached content in seconds. When set, cached content older than this threshold is ignored and the page is re-fetched. Must be 0 or greater. Default: null (no age limit, cached content is returned regardless of age). | 86400 | \ No newline at end of file diff --git a/docs/models/contenttype.md b/docs/models/contenttype.md index b043bcf..02de462 100644 --- a/docs/models/contenttype.md +++ b/docs/models/contenttype.md @@ -2,9 +2,18 @@ The format of the content field. +## Example Usage + +```python +from youdotcom.models import ContentType + +value = ContentType.TEXT +``` + ## Values -| Name | Value | -| ------ | ------ | -| `TEXT` | text | \ No newline at end of file +| Name | Value | +| -------- | -------- | +| `TEXT` | text | +| `OBJECT` | object | \ No newline at end of file diff --git a/docs/models/contentunion.md b/docs/models/contentunion.md new file mode 100644 index 0000000..4ad6922 --- /dev/null +++ b/docs/models/contentunion.md @@ -0,0 +1,19 @@ +# ContentUnion + +The comprehensive response with inline citations. When content_type is "text", this is a Markdown string with numbered citations that reference the items in the sources array. When content_type is "object", this is a structured JSON object matching the requested output_schema. + + +## Supported Types + +### `str` + +```python +value: str = /* values here */ +``` + +### `models.Content` + +```python +value: models.Content = /* values here */ +``` + diff --git a/docs/models/country.md b/docs/models/country.md index a4fc0c4..1bee618 100644 --- a/docs/models/country.md +++ b/docs/models/country.md @@ -2,6 +2,14 @@ The country code that determines the geographical focus of the web results. +## Example Usage + +```python +from youdotcom.models import Country + +value = Country.AR +``` + ## Values diff --git a/docs/models/event.md b/docs/models/event.md new file mode 100644 index 0000000..c8bc8a7 --- /dev/null +++ b/docs/models/event.md @@ -0,0 +1,25 @@ +# Event + +The type of the SSE event. Most streams start with a `connected` event and then deliver terminal events `response.done`, `complete`, `error`, or `cancelled` from the worker. +If the SSE stream has aged out (after ~15 minutes) without any events flowing and the task is already in a terminal state, the server emits a synthetic event whose name is the task's status: one of `completed`, `failed`, or `cancelled`. Treat these synthetic event names the same as the corresponding worker-emitted names (`complete` ↔ `completed`, `error` ↔ `failed`, `cancelled` == `cancelled`). + +## Example Usage + +```python +from youdotcom.models import Event + +value = Event.CONNECTED +``` + + +## Values + +| Name | Value | +| --------------- | --------------- | +| `CONNECTED` | connected | +| `RESPONSE_DONE` | response.done | +| `COMPLETE` | complete | +| `COMPLETED` | completed | +| `ERROR` | error | +| `FAILED` | failed | +| `CANCELLED` | cancelled | \ No newline at end of file diff --git a/docs/models/financeresearchcontenttype.md b/docs/models/financeresearchcontenttype.md new file mode 100644 index 0000000..eba2d2d --- /dev/null +++ b/docs/models/financeresearchcontenttype.md @@ -0,0 +1,18 @@ +# FinanceResearchContentType + +The format of the content field. + +## Example Usage + +```python +from youdotcom.models import FinanceResearchContentType + +value = FinanceResearchContentType.TEXT +``` + + +## Values + +| Name | Value | +| ------ | ------ | +| `TEXT` | text | \ No newline at end of file diff --git a/docs/models/financeresearchdetail.md b/docs/models/financeresearchdetail.md new file mode 100644 index 0000000..ca5a772 --- /dev/null +++ b/docs/models/financeresearchdetail.md @@ -0,0 +1,12 @@ +# FinanceResearchDetail + + +## Fields + +| Field | Type | Required | Description | Example | +| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `type` | *str* | :heavy_check_mark: | The validation error type. | missing | +| `loc` | List[[models.FinanceResearchLoc](../models/financeresearchloc.md)] | :heavy_check_mark: | The location of the error as a path of segments (strings for field names, integers for byte offsets). | [
"body",
"input"
] | +| `msg` | *str* | :heavy_check_mark: | A human-readable description of the error. | Field required | +| `input` | [models.FinanceResearchInputUnion](../models/financeresearchinputunion.md) | :heavy_check_mark: | The input value that caused the error. | | +| `ctx` | Dict[str, *Any*] | :heavy_minus_sign: | Additional context about the error. | | \ No newline at end of file diff --git a/docs/models/financeresearcheffort.md b/docs/models/financeresearcheffort.md new file mode 100644 index 0000000..9402be1 --- /dev/null +++ b/docs/models/financeresearcheffort.md @@ -0,0 +1,23 @@ +# FinanceResearchEffort + +Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + +Available levels: +- `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research. +- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. + +## Example Usage + +```python +from youdotcom.models import FinanceResearchEffort + +value = FinanceResearchEffort.DEEP +``` + + +## Values + +| Name | Value | +| ------------ | ------------ | +| `DEEP` | deep | +| `EXHAUSTIVE` | exhaustive | \ No newline at end of file diff --git a/docs/models/financeresearchinput.md b/docs/models/financeresearchinput.md new file mode 100644 index 0000000..c7fc559 --- /dev/null +++ b/docs/models/financeresearchinput.md @@ -0,0 +1,7 @@ +# FinanceResearchInput + + +## Fields + +| Field | Type | Required | Description | +| ----------- | ----------- | ----------- | ----------- | \ No newline at end of file diff --git a/docs/models/financeresearchinputunion.md b/docs/models/financeresearchinputunion.md new file mode 100644 index 0000000..f6a582f --- /dev/null +++ b/docs/models/financeresearchinputunion.md @@ -0,0 +1,19 @@ +# FinanceResearchInputUnion + +The input value that caused the error. + + +## Supported Types + +### `str` + +```python +value: str = /* values here */ +``` + +### `models.FinanceResearchInput` + +```python +value: models.FinanceResearchInput = /* values here */ +``` + diff --git a/docs/models/financeresearchloc.md b/docs/models/financeresearchloc.md new file mode 100644 index 0000000..df54403 --- /dev/null +++ b/docs/models/financeresearchloc.md @@ -0,0 +1,17 @@ +# FinanceResearchLoc + + +## Supported Types + +### `str` + +```python +value: str = /* values here */ +``` + +### `int` + +```python +value: int = /* values here */ +``` + diff --git a/docs/models/financeresearchoutput.md b/docs/models/financeresearchoutput.md new file mode 100644 index 0000000..b6e3528 --- /dev/null +++ b/docs/models/financeresearchoutput.md @@ -0,0 +1,12 @@ +# FinanceResearchOutput + +The research output containing the answer and sources. + + +## Fields + +| Field | Type | Required | Description | Example | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `content` | *str* | :heavy_check_mark: | The comprehensive finance-grade response with inline citations. Content is a Markdown string with numbered citations that reference the items in the sources array. | For fiscal year 2025, ended January 26, 2025, NVIDIA's revenue rose to **$130.5 billion, up 114% year over year**.[[1]]
The main drivers were Data Center demand (up 142%), Compute & Networking growth (up 145%), and smaller contributions from Gaming, Professional Visualization, and Automotive. | +| `content_type` | [models.FinanceResearchContentType](../models/financeresearchcontenttype.md) | :heavy_check_mark: | The format of the content field. | text | +| `sources` | List[[models.FinanceResearchSource](../models/financeresearchsource.md)] | :heavy_check_mark: | A list of web sources used to generate the answer. | | \ No newline at end of file diff --git a/docs/models/financeresearchrequest.md b/docs/models/financeresearchrequest.md new file mode 100644 index 0000000..7342a4f --- /dev/null +++ b/docs/models/financeresearchrequest.md @@ -0,0 +1,9 @@ +# FinanceResearchRequest + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `input` | *str* | :heavy_check_mark: | The financial research question or complex query requiring in-depth investigation and multi-step reasoning.

Note: The maximum length of the input is 40,000 characters. | What were the key drivers of NVIDIA's revenue growth in fiscal year 2025? | +| `research_effort` | [Optional[models.FinanceResearchEffort]](../models/financeresearcheffort.md) | :heavy_minus_sign: | Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time.

Available levels:
- `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research.
- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. | deep | \ No newline at end of file diff --git a/docs/models/financeresearchresponse.md b/docs/models/financeresearchresponse.md new file mode 100644 index 0000000..7db9645 --- /dev/null +++ b/docs/models/financeresearchresponse.md @@ -0,0 +1,10 @@ +# FinanceResearchResponse + +A JSON object containing a comprehensive finance-grade answer with citations and supporting search results + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| `output` | [models.FinanceResearchOutput](../models/financeresearchoutput.md) | :heavy_check_mark: | The research output containing the answer and sources. | \ No newline at end of file diff --git a/docs/models/financeresearchsource.md b/docs/models/financeresearchsource.md new file mode 100644 index 0000000..90a8e2a --- /dev/null +++ b/docs/models/financeresearchsource.md @@ -0,0 +1,9 @@ +# FinanceResearchSource + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| `url` | *str* | :heavy_check_mark: | The URL of the source webpage. | https://investor.apple.com/sec-filings/annual-reports/default.aspx | +| `title` | *Optional[str]* | :heavy_minus_sign: | The title of the source webpage. | Apple Inc. Annual Report FY2024 (Form 10-K) | \ No newline at end of file diff --git a/docs/models/freshness.md b/docs/models/freshness.md index e3b507d..38377ef 100644 --- a/docs/models/freshness.md +++ b/docs/models/freshness.md @@ -1,6 +1,12 @@ # Freshness -Specifies the freshness of the results to return. +## Example Usage + +```python +from youdotcom.models import Freshness + +value = Freshness.DAY +``` ## Values diff --git a/docs/models/searchfreshness.md b/docs/models/freshnessvalue.md similarity index 97% rename from docs/models/searchfreshness.md rename to docs/models/freshnessvalue.md index e947818..4c5c058 100644 --- a/docs/models/searchfreshness.md +++ b/docs/models/freshnessvalue.md @@ -1,4 +1,4 @@ -# SearchFreshness +# FreshnessValue Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. diff --git a/docs/models/getresearchtaskrequest.md b/docs/models/getresearchtaskrequest.md new file mode 100644 index 0000000..ba0a7b2 --- /dev/null +++ b/docs/models/getresearchtaskrequest.md @@ -0,0 +1,8 @@ +# GetResearchTaskRequest + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------ | ------------------------------ | ------------------------------ | ------------------------------ | +| `task_id` | *str* | :heavy_check_mark: | The UUID of the research task. | \ No newline at end of file diff --git a/docs/models/language.md b/docs/models/language.md index 862060b..b8d19c1 100644 --- a/docs/models/language.md +++ b/docs/models/language.md @@ -1,5 +1,15 @@ # Language +The language of the web results that will be returned (BCP 47 format). + +## Example Usage + +```python +from youdotcom.models import Language + +value = Language.AR +``` + ## Values @@ -30,7 +40,7 @@ | `HU` | HU | | `IS` | IS | | `IT` | IT | -| `JP` | JP | +| `JA` | JA | | `KN` | KN | | `KO` | KO | | `LV` | LV | diff --git a/docs/models/livecrawl.md b/docs/models/livecrawl.md index ff4239a..c62c4ee 100644 --- a/docs/models/livecrawl.md +++ b/docs/models/livecrawl.md @@ -2,6 +2,14 @@ Indicates which section(s) of search results to livecrawl and return full page content. +## Example Usage + +```python +from youdotcom.models import LiveCrawl + +value = LiveCrawl.WEB +``` + ## Values diff --git a/docs/models/livecrawlformats.md b/docs/models/livecrawlformats.md index dcfb112..c2d0a6b 100644 --- a/docs/models/livecrawlformats.md +++ b/docs/models/livecrawlformats.md @@ -1,6 +1,12 @@ # LiveCrawlFormats -Indicates the format of the livecrawled content. +## Example Usage + +```python +from youdotcom.models import LiveCrawlFormats + +value = LiveCrawlFormats.HTML +``` ## Values diff --git a/docs/models/news.md b/docs/models/newsresult.md similarity index 99% rename from docs/models/news.md rename to docs/models/newsresult.md index fec0835..0aad594 100644 --- a/docs/models/news.md +++ b/docs/models/newsresult.md @@ -1,4 +1,4 @@ -# News +# NewsResult ## Fields diff --git a/docs/models/output.md b/docs/models/output.md index d7c2771..a47cad9 100644 --- a/docs/models/output.md +++ b/docs/models/output.md @@ -5,8 +5,8 @@ The research output containing the answer and sources. ## Fields -| Field | Type | Required | Description | -| --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `content` | *str* | :heavy_check_mark: | The comprehensive response with inline citations. The content is formatted in Markdown and includes numbered citations that reference the items in the sources array. | -| `content_type` | [models.ContentType](../models/contenttype.md) | :heavy_check_mark: | The format of the content field. | -| `sources` | List[[models.Source](../models/source.md)] | :heavy_check_mark: | A list of web sources used to generate the answer. | \ No newline at end of file +| Field | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `content` | [models.ContentUnion](../models/contentunion.md) | :heavy_check_mark: | The comprehensive response with inline citations. When content_type is "text", this is a Markdown string with numbered citations that reference the items in the sources array. When content_type is "object", this is a structured JSON object matching the requested output_schema. | +| `content_type` | [models.ContentType](../models/contenttype.md) | :heavy_check_mark: | The format of the content field. | +| `sources` | List[[models.Source](../models/source.md)] | :heavy_check_mark: | A list of web sources used to generate the answer. | \ No newline at end of file diff --git a/docs/models/outputschema.md b/docs/models/outputschema.md new file mode 100644 index 0000000..44443ec --- /dev/null +++ b/docs/models/outputschema.md @@ -0,0 +1,13 @@ +# OutputSchema + +Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: "lite" returns 422. + +Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported. + +Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. + + +## Fields + +| Field | Type | Required | Description | +| ----------- | ----------- | ----------- | ----------- | \ No newline at end of file diff --git a/docs/models/reportverbosity.md b/docs/models/reportverbosity.md index 1a2aae5..7c3d17f 100644 --- a/docs/models/reportverbosity.md +++ b/docs/models/reportverbosity.md @@ -2,6 +2,14 @@ Select whether to receive a medium or high length model response. +## Example Usage + +```python +from youdotcom.models import ReportVerbosity + +value = ReportVerbosity.MEDIUM +``` + ## Values diff --git a/docs/models/researchdetail.md b/docs/models/researchdetail.md index e635556..8ee1a3b 100644 --- a/docs/models/researchdetail.md +++ b/docs/models/researchdetail.md @@ -8,5 +8,5 @@ | `type` | *str* | :heavy_check_mark: | The validation error type. | missing | | `loc` | List[[models.ResearchLoc](../models/researchloc.md)] | :heavy_check_mark: | The location of the error as a path of segments (strings for field names, integers for byte offsets). | [
"body",
"input"
] | | `msg` | *str* | :heavy_check_mark: | A human-readable description of the error. | Field required | -| `input` | [models.Input2](../models/input2.md) | :heavy_check_mark: | The input value that caused the error. | | +| `input` | [models.ResearchInputUnion](../models/researchinputunion.md) | :heavy_check_mark: | The input value that caused the error. | | | `ctx` | Dict[str, *Any*] | :heavy_minus_sign: | Additional context about the error. | | \ No newline at end of file diff --git a/docs/models/researcheffort.md b/docs/models/researcheffort.md index e54e207..27ca90d 100644 --- a/docs/models/researcheffort.md +++ b/docs/models/researcheffort.md @@ -8,6 +8,14 @@ Available levels: - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. +## Example Usage + +```python +from youdotcom.models import ResearchEffort + +value = ResearchEffort.LITE +``` + ## Values diff --git a/docs/models/input2.md b/docs/models/researchinputunion.md similarity index 90% rename from docs/models/input2.md rename to docs/models/researchinputunion.md index fefc4e5..182ea14 100644 --- a/docs/models/input2.md +++ b/docs/models/researchinputunion.md @@ -1,4 +1,4 @@ -# Input2 +# ResearchInputUnion The input value that caused the error. diff --git a/docs/models/researchrequest.md b/docs/models/researchrequest.md index be8ad6e..d5f7ba3 100644 --- a/docs/models/researchrequest.md +++ b/docs/models/researchrequest.md @@ -6,4 +6,7 @@ | Field | Type | Required | Description | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `input` | *str* | :heavy_check_mark: | The research question or complex query requiring in-depth investigation and multi-step reasoning.

Note: The maximum length of the input is 40,000 characters. | -| `research_effort` | [Optional[models.ResearchEffort]](../models/researcheffort.md) | :heavy_minus_sign: | Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time.

Available levels:
- `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer.
- `standard`: The default. Balances speed and depth, a good fit for most questions.
- `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed.
- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. | \ No newline at end of file +| `research_effort` | [Optional[models.ResearchEffort]](../models/researcheffort.md) | :heavy_minus_sign: | Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time.

Available levels:
- `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer.
- `standard`: The default. Balances speed and depth, a good fit for most questions.
- `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed.
- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. | +| `background` | *Optional[bool]* | :heavy_minus_sign: | When true, queue a research task and return a task handle immediately instead of waiting for the result inline. Defaults to synchronous. When enabled, the response is a TaskResponse object with a task_id and stream_url for polling progress via GET /v1/research/{task_id} or streaming via GET /v1/research/{task_id}/stream. | +| `source_control` | [Optional[models.SourceControl]](../models/sourcecontrol.md) | :heavy_minus_sign: | Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country.

`include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. | +| `output_schema` | [Optional[models.OutputSchema]](../models/outputschema.md) | :heavy_minus_sign: | Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: "lite" returns 422.

Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported.

Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. | \ No newline at end of file diff --git a/docs/models/researchresponse.md b/docs/models/researchresponse.md index 58a1bc3..e46094c 100644 --- a/docs/models/researchresponse.md +++ b/docs/models/researchresponse.md @@ -1,7 +1,5 @@ # ResearchResponse -A JSON object containing a comprehensive answer with citations and supporting search results - ## Fields diff --git a/docs/models/researchresponse1.md b/docs/models/researchresponse1.md new file mode 100644 index 0000000..b625262 --- /dev/null +++ b/docs/models/researchresponse1.md @@ -0,0 +1,19 @@ +# ResearchResponse1 + +A JSON object containing a comprehensive answer with citations and supporting search results. When background=true, returns a task handle instead. + + +## Supported Types + +### `models.ResearchResponse` + +```python +value: models.ResearchResponse = /* values here */ +``` + +### `models.TaskResponse` + +```python +value: models.TaskResponse = /* values here */ +``` + diff --git a/docs/models/researchtaskstreamevent.md b/docs/models/researchtaskstreamevent.md new file mode 100644 index 0000000..f9fafb0 --- /dev/null +++ b/docs/models/researchtaskstreamevent.md @@ -0,0 +1,12 @@ +# ResearchTaskStreamEvent + +A server-sent event for a background research task stream. + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | *str* | :heavy_check_mark: | Sequence number of the SSE event. | +| `event` | [models.Event](../models/event.md) | :heavy_check_mark: | The type of the SSE event. Most streams start with a `connected` event and then deliver terminal events `response.done`, `complete`, `error`, or `cancelled` from the worker.
If the SSE stream has aged out (after ~15 minutes) without any events flowing and the task is already in a terminal state, the server emits a synthetic event whose name is the task's status: one of `completed`, `failed`, or `cancelled`. Treat these synthetic event names the same as the corresponding worker-emitted names (`complete` ↔ `completed`, `error` ↔ `failed`, `cancelled` == `cancelled`). | +| `data` | [models.ResearchTaskStreamEventData](../models/researchtaskstreameventdata.md) | :heavy_check_mark: | The event payload. Structure varies by event type. Common fields include type, task_id, status, data (event-specific), error, and sequence. | \ No newline at end of file diff --git a/docs/models/researchtaskstreameventdata.md b/docs/models/researchtaskstreameventdata.md new file mode 100644 index 0000000..56634db --- /dev/null +++ b/docs/models/researchtaskstreameventdata.md @@ -0,0 +1,15 @@ +# ResearchTaskStreamEventData + +The event payload. Structure varies by event type. Common fields include type, task_id, status, data (event-specific), error, and sequence. + + +## Fields + +| Field | Type | Required | Description | +| ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | +| `type` | *Optional[str]* | :heavy_minus_sign: | The event type identifier. | +| `task_id` | *Optional[str]* | :heavy_minus_sign: | The task UUID. | +| `status` | *Optional[str]* | :heavy_minus_sign: | Current task status when the event was emitted. | +| `data` | Dict[str, *Any*] | :heavy_minus_sign: | Event-specific payload data. | +| `error` | *OptionalNullable[str]* | :heavy_minus_sign: | Error message if the event represents an error. | +| `sequence` | *Optional[int]* | :heavy_minus_sign: | Event sequence number. | \ No newline at end of file diff --git a/docs/models/result.md b/docs/models/result.md new file mode 100644 index 0000000..bca20b4 --- /dev/null +++ b/docs/models/result.md @@ -0,0 +1,9 @@ +# Result + +The task result when completed. For research tasks, this contains the ResearchResponse output. + + +## Fields + +| Field | Type | Required | Description | +| ----------- | ----------- | ----------- | ----------- | \ No newline at end of file diff --git a/docs/models/results.md b/docs/models/results.md index c178f25..342f0ea 100644 --- a/docs/models/results.md +++ b/docs/models/results.md @@ -3,7 +3,7 @@ ## Fields -| Field | Type | Required | Description | -| -------------------------------------- | -------------------------------------- | -------------------------------------- | -------------------------------------- | -| `web` | List[[models.Web](../models/web.md)] | :heavy_minus_sign: | N/A | -| `news` | List[[models.News](../models/news.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | +| `web` | List[[models.WebResult](../models/webresult.md)] | :heavy_minus_sign: | N/A | +| `news` | List[[models.NewsResult](../models/newsresult.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/role.md b/docs/models/role.md index 3313880..1f861cc 100644 --- a/docs/models/role.md +++ b/docs/models/role.md @@ -2,6 +2,14 @@ The access based role of the user +## Example Usage + +```python +from youdotcom.models import Role + +value = Role.USER +``` + ## Values diff --git a/docs/models/safesearch.md b/docs/models/safesearch.md index 2eae368..9efddcb 100644 --- a/docs/models/safesearch.md +++ b/docs/models/safesearch.md @@ -2,6 +2,14 @@ Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. +## Example Usage + +```python +from youdotcom.models import SafeSearch + +value = SafeSearch.OFF +``` + ## Values diff --git a/docs/models/searchcountry.md b/docs/models/searchcountry.md deleted file mode 100644 index 9a95aec..0000000 --- a/docs/models/searchcountry.md +++ /dev/null @@ -1,19 +0,0 @@ -# SearchCountry - -The country code that determines the geographical focus of the web results. - - -## Supported Types - -### `models.Country` - -```python -value: models.Country = /* values here */ -``` - -### `str` - -```python -value: str = /* values here */ -``` - diff --git a/docs/models/searcheffort.md b/docs/models/searcheffort.md index 95bb5b7..6f1c1ed 100644 --- a/docs/models/searcheffort.md +++ b/docs/models/searcheffort.md @@ -4,6 +4,14 @@ This parameter maps to different configurations regarding the depth of research Alternatively, use `auto` mode for a more dynamic search approach, allowing the tool the freedom to adjust its subparameters. +## Example Usage + +```python +from youdotcom.models import SearchEffort + +value = SearchEffort.AUTO +``` + ## Values diff --git a/docs/models/searchlivecrawl.md b/docs/models/searchlivecrawl.md deleted file mode 100644 index ccf9200..0000000 --- a/docs/models/searchlivecrawl.md +++ /dev/null @@ -1,19 +0,0 @@ -# SearchLivecrawl - -Indicates which section(s) of search results to livecrawl and return full page content. - - -## Supported Types - -### `models.LiveCrawl` - -```python -value: models.LiveCrawl = /* values here */ -``` - -### `str` - -```python -value: str = /* values here */ -``` - diff --git a/docs/models/searchlivecrawlformats.md b/docs/models/searchlivecrawlformats.md deleted file mode 100644 index 690de1f..0000000 --- a/docs/models/searchlivecrawlformats.md +++ /dev/null @@ -1,19 +0,0 @@ -# SearchLivecrawlFormats - -Indicates the format of the livecrawled content. - - -## Supported Types - -### `models.LiveCrawlFormats` - -```python -value: models.LiveCrawlFormats = /* values here */ -``` - -### `str` - -```python -value: str = /* values here */ -``` - diff --git a/docs/models/metadata.md b/docs/models/searchmetadata.md similarity index 92% rename from docs/models/metadata.md rename to docs/models/searchmetadata.md index 66efc64..0110563 100644 --- a/docs/models/metadata.md +++ b/docs/models/searchmetadata.md @@ -1,4 +1,4 @@ -# Metadata +# SearchMetadata ## Fields @@ -6,5 +6,5 @@ | Field | Type | Required | Description | Example | | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | | `search_uuid` | *Optional[str]* | :heavy_minus_sign: | N/A | 942ccbdd-7705-4d9c-9d37-4ef386658e90 | -| `query` | *Optional[str]* | :heavy_minus_sign: | Returns the search query used to retrieve the results. | Your query | +| `query` | *Optional[str]* | :heavy_minus_sign: | Returns the search query used to retrieve the results. | What are the latest geopolitical updates from India | | `latency` | *Optional[float]* | :heavy_minus_sign: | N/A | 0.123 | \ No newline at end of file diff --git a/docs/models/searchrequest.md b/docs/models/searchrequest.md index 1adb45e..3c07e4c 100644 --- a/docs/models/searchrequest.md +++ b/docs/models/searchrequest.md @@ -5,12 +5,16 @@ | Field | Type | Required | Description | Example | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | Your query | -| `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | -| `freshness` | [Optional[models.SearchFreshness]](../models/searchfreshness.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | -| `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | -| `country` | [Optional[models.SearchCountry]](../models/searchcountry.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | +| `query` | *str* | :heavy_check_mark: | N/A | What are the latest geopolitical updates from India | +| `count` | *Optional[int]* | :heavy_minus_sign: | N/A | | +| `freshness` | [Optional[models.FreshnessValue]](../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | +| `offset` | *Optional[int]* | :heavy_minus_sign: | N/A | | +| `country` | [Optional[models.Country]](../models/country.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | | `language` | [Optional[models.Language]](../models/language.md) | :heavy_minus_sign: | The language of the web results that will be returned (BCP 47 format). | | -| `safesearch` | [Optional[models.SearchSafesearch]](../models/searchsafesearch.md) | :heavy_minus_sign: | Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. | | -| `livecrawl` | [Optional[models.SearchLivecrawl]](../models/searchlivecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | -| `livecrawl_formats` | [Optional[models.SearchLivecrawlFormats]](../models/searchlivecrawlformats.md) | :heavy_minus_sign: | Indicates the format of the livecrawled content. | | \ No newline at end of file +| `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. | | +| `livecrawl` | [Optional[models.LiveCrawl]](../models/livecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | +| `livecrawl_formats` | List[[models.LiveCrawlFormats](../models/livecrawlformats.md)] | :heavy_minus_sign: | N/A | | +| `include_domains` | *Optional[str]* | :heavy_minus_sign: | A list of domains to restrict search results to. Only results from these domains will be returned. For large domain lists (up to 500), use POST with a JSON array instead. This is a strict allowlist — cannot be combined with `exclude_domains` (returns `422`).

**Important:** Use a single comma-separated value (e.g. `include_domains=nytimes.com,bbc.com`). Repeated parameters (`include_domains=a.com&include_domains=b.com`) are not supported. | nytimes.com,bbc.com | +| `exclude_domains` | *Optional[str]* | :heavy_minus_sign: | A list of domains to exclude from search results. Results from these domains will be filtered out. For large domain lists (up to 500), use POST with a JSON array instead. Cannot be combined with `include_domains` (returns `422`).

**Important:** You must use a single comma-separated value (e.g. `exclude_domains=spam-site.com,other-site.com`). Repeated parameters are not supported. | spam-site.com,other-site.com | +| `boost_domains` | *Optional[str]* | :heavy_minus_sign: | A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`).

**Important:** You must use a single comma-separated value (e.g. `boost_domains=nytimes.com,wired.com`). Repeated parameters are not supported. | nytimes.com,wired.com | +| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | N/A | 10 | \ No newline at end of file diff --git a/docs/models/searchrequestbody.md b/docs/models/searchrequestbody.md new file mode 100644 index 0000000..82a0428 --- /dev/null +++ b/docs/models/searchrequestbody.md @@ -0,0 +1,20 @@ +# SearchRequestBody + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | What are the latest geopolitical updates from India | +| `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | +| `freshness` | [Optional[models.FreshnessValue]](../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | +| `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | +| `country` | [Optional[models.Country]](../models/country.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | +| `language` | [Optional[models.Language]](../models/language.md) | :heavy_minus_sign: | The language of the web results that will be returned (BCP 47 format). | | +| `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. | | +| `livecrawl` | [Optional[models.LiveCrawl]](../models/livecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | +| `livecrawl_formats` | List[[models.LiveCrawlFormats](../models/livecrawlformats.md)] | :heavy_minus_sign: | Indicates the format(s) of the livecrawled content. Pass one or both values (`html`, `markdown`). In a GET request, repeat the parameter: `?livecrawl_formats=html&livecrawl_formats=markdown`. In a POST body, provide a JSON array: `["html", "markdown"]`. | | +| `include_domains` | List[*str*] | :heavy_minus_sign: | A list of domains to restrict search results to. Only results from these domains will be returned. Supports up to 500 domains. This is a strict allowlist, not a boost — results are limited exclusively to the specified domains.

Cannot be combined with `exclude_domains`; passing both will return a `422` error. | [
"nytimes.com",
"bbc.com"
] | +| `exclude_domains` | List[*str*] | :heavy_minus_sign: | A list of domains to exclude from search results. Results from these domains will be filtered out. Supports up to 500 domains.

Cannot be combined with `include_domains`; passing both will return a `422` error. | [
"spam-site.com",
"other-site.com"
] | +| `boost_domains` | List[*str*] | :heavy_minus_sign: | A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). | [
"nytimes.com",
"wired.com"
] | +| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | Maximum time in seconds to wait for page content when `livecrawl` is enabled. Must be between 1 and 60 seconds. Default is 10 seconds. | 10 | \ No newline at end of file diff --git a/docs/models/searchresponse.md b/docs/models/searchresponse.md index 5a85ee3..0a0dcb7 100644 --- a/docs/models/searchresponse.md +++ b/docs/models/searchresponse.md @@ -5,7 +5,7 @@ A JSON object containing unified search results from web and news sources ## Fields -| Field | Type | Required | Description | -| -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -| `results` | [Optional[models.Results]](../models/results.md) | :heavy_minus_sign: | N/A | -| `metadata` | [Optional[models.Metadata]](../models/metadata.md) | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- | +| `results` | [Optional[models.Results]](../models/results.md) | :heavy_minus_sign: | N/A | +| `metadata` | [Optional[models.SearchMetadata]](../models/searchmetadata.md) | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/models/searchsafesearch.md b/docs/models/searchsafesearch.md deleted file mode 100644 index fdad51a..0000000 --- a/docs/models/searchsafesearch.md +++ /dev/null @@ -1,19 +0,0 @@ -# SearchSafesearch - -Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. - - -## Supported Types - -### `models.SafeSearch` - -```python -value: models.SafeSearch = /* values here */ -``` - -### `str` - -```python -value: str = /* values here */ -``` - diff --git a/docs/models/sourcecontrol.md b/docs/models/sourcecontrol.md new file mode 100644 index 0000000..cc42a85 --- /dev/null +++ b/docs/models/sourcecontrol.md @@ -0,0 +1,16 @@ +# SourceControl + +Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country. + +`include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. + + +## Fields + +| Field | Type | Required | Description | +| ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `include_domains` | List[*str*] | :heavy_minus_sign: | Only return results from these domains. Max 500 domains. Cannot be used with exclude_domains or boost_domains. | +| `exclude_domains` | List[*str*] | :heavy_minus_sign: | Never return results from these domains. Max 500 domains. Also blocks the research agent from visiting pages on those domains during browsing. | +| `boost_domains` | List[*str*] | :heavy_minus_sign: | Boost results from these domains without excluding other domains. Max 500 domains. Cannot be used with include_domains. | +| `freshness` | *Optional[str]* | :heavy_minus_sign: | Filter results by recency. Accepts `day`, `week`, `month`, `year`, or a custom date range in `YYYY-MM-DDtoYYYY-MM-DD` format. | +| `country` | *Optional[str]* | :heavy_minus_sign: | ISO 3166-1 alpha-2 country code, such as US, GB, or DE, to geographically focus web results. | \ No newline at end of file diff --git a/docs/models/streamresearchtaskrequest.md b/docs/models/streamresearchtaskrequest.md new file mode 100644 index 0000000..1b8c0b3 --- /dev/null +++ b/docs/models/streamresearchtaskrequest.md @@ -0,0 +1,9 @@ +# StreamResearchTaskRequest + + +## Fields + +| Field | Type | Required | Description | +| ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | +| `task_id` | *str* | :heavy_check_mark: | The UUID of the research task. | +| `from_id` | *Optional[int]* | :heavy_minus_sign: | Resume from a sequence number for reconnection. | \ No newline at end of file diff --git a/docs/models/taskdetail.md b/docs/models/taskdetail.md new file mode 100644 index 0000000..a153ed2 --- /dev/null +++ b/docs/models/taskdetail.md @@ -0,0 +1,16 @@ +# TaskDetail + + +## Fields + +| Field | Type | Required | Description | Example | +| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `id` | *str* | :heavy_check_mark: | Unique identifier for the task. | | +| `task_type` | *str* | :heavy_check_mark: | Task type. | research | +| `status` | [models.TaskDetailStatus](../models/taskdetailstatus.md) | :heavy_check_mark: | Current status of the task. | | +| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_check_mark: | When the task was created. | | +| `updated_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_check_mark: | When the task was last updated. | | +| `completed_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_minus_sign: | When the task completed, if applicable. | | +| `error` | *OptionalNullable[str]* | :heavy_minus_sign: | Error message if the task failed. | | +| `input` | [OptionalNullable[models.TaskDetailInput]](../models/taskdetailinput.md) | :heavy_minus_sign: | The original request input for the task. | | +| `result` | [OptionalNullable[models.Result]](../models/result.md) | :heavy_minus_sign: | The task result when completed. For research tasks, this contains the ResearchResponse output. | | \ No newline at end of file diff --git a/docs/models/taskdetailinput.md b/docs/models/taskdetailinput.md new file mode 100644 index 0000000..4470b82 --- /dev/null +++ b/docs/models/taskdetailinput.md @@ -0,0 +1,9 @@ +# TaskDetailInput + +The original request input for the task. + + +## Fields + +| Field | Type | Required | Description | +| ----------- | ----------- | ----------- | ----------- | \ No newline at end of file diff --git a/docs/models/taskdetailstatus.md b/docs/models/taskdetailstatus.md new file mode 100644 index 0000000..94533f5 --- /dev/null +++ b/docs/models/taskdetailstatus.md @@ -0,0 +1,22 @@ +# TaskDetailStatus + +Current status of the task. + +## Example Usage + +```python +from youdotcom.models import TaskDetailStatus + +value = TaskDetailStatus.QUEUED +``` + + +## Values + +| Name | Value | +| ----------- | ----------- | +| `QUEUED` | queued | +| `RUNNING` | running | +| `COMPLETED` | completed | +| `FAILED` | failed | +| `CANCELLED` | cancelled | \ No newline at end of file diff --git a/docs/models/taskresponse.md b/docs/models/taskresponse.md new file mode 100644 index 0000000..759621a --- /dev/null +++ b/docs/models/taskresponse.md @@ -0,0 +1,12 @@ +# TaskResponse + + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `task_id` | *str* | :heavy_check_mark: | Unique identifier for the task. | | +| `type` | *str* | :heavy_check_mark: | Task type. | research | +| `status` | [models.TaskResponseStatus](../models/taskresponsestatus.md) | :heavy_check_mark: | Current status of the task. | queued | +| `stream_url` | *str* | :heavy_check_mark: | URL to stream task events via SSE. | /v1/research/a1b2c3d4-0000-0000-0000-000000000000/stream | +| `created_at` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_check_mark: | When the task was created. | | \ No newline at end of file diff --git a/docs/models/taskresponsestatus.md b/docs/models/taskresponsestatus.md new file mode 100644 index 0000000..b6ee96e --- /dev/null +++ b/docs/models/taskresponsestatus.md @@ -0,0 +1,22 @@ +# TaskResponseStatus + +Current status of the task. + +## Example Usage + +```python +from youdotcom.models import TaskResponseStatus + +value = TaskResponseStatus.QUEUED +``` + + +## Values + +| Name | Value | +| ----------- | ----------- | +| `QUEUED` | queued | +| `RUNNING` | running | +| `COMPLETED` | completed | +| `FAILED` | failed | +| `CANCELLED` | cancelled | \ No newline at end of file diff --git a/docs/models/type.md b/docs/models/type.md index db75a34..31b2152 100644 --- a/docs/models/type.md +++ b/docs/models/type.md @@ -4,6 +4,14 @@ The type of output. This can either be: * `message.answer` for text responses * `web_search.results` for output that contains web links. `web_search.results` only appear when you use the `research` tool or express agent with web_search +## Example Usage + +```python +from youdotcom.models import Type + +value = Type.MESSAGE_ANSWER +``` + ## Values diff --git a/docs/models/verbosity.md b/docs/models/verbosity.md index 3c30d67..35bfe62 100644 --- a/docs/models/verbosity.md +++ b/docs/models/verbosity.md @@ -2,6 +2,14 @@ Controls the level of detail provided by the agent's response. Choosing high maps to a long-form report while medium maps to a medium verbosity report that captures most details but is less comprehensive. +## Example Usage + +```python +from youdotcom.models import Verbosity + +value = Verbosity.MEDIUM +``` + ## Values diff --git a/docs/models/web.md b/docs/models/webresult.md similarity index 99% rename from docs/models/web.md rename to docs/models/webresult.md index b66b065..efac9df 100644 --- a/docs/models/web.md +++ b/docs/models/webresult.md @@ -1,4 +1,4 @@ -# Web +# WebResult ## Fields diff --git a/docs/sdks/contentssdk/README.md b/docs/sdks/contentssdk/README.md index e4ce470..b1617a1 100644 --- a/docs/sdks/contentssdk/README.md +++ b/docs/sdks/contentssdk/README.md @@ -19,7 +19,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.contents.generate(urls=[ @@ -27,7 +27,7 @@ with You( ], formats=[ models.ContentsFormats.HTML, models.ContentsFormats.MARKDOWN, - ], crawl_timeout=10) + ], crawl_timeout=10, max_age=86400) # Handle response print(res) @@ -42,7 +42,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.contents.generate(urls=[ @@ -50,7 +50,7 @@ with You( ], formats=[ models.ContentsFormats.HTML, models.ContentsFormats.MARKDOWN, - ], crawl_timeout=10) + ], crawl_timeout=10, max_age=86400) # Handle response print(res) @@ -65,7 +65,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.contents.generate(urls=[ @@ -73,7 +73,7 @@ with You( ], formats=[ models.ContentsFormats.HTML, models.ContentsFormats.MARKDOWN, - ], crawl_timeout=10) + ], crawl_timeout=10, max_age=86400) # Handle response print(res) @@ -88,7 +88,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.contents.generate(urls=[ @@ -96,7 +96,7 @@ with You( ], formats=[ models.ContentsFormats.HTML, models.ContentsFormats.MARKDOWN, - ], crawl_timeout=10) + ], crawl_timeout=10, max_age=86400) # Handle response print(res) @@ -111,7 +111,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.contents.generate(urls=[ @@ -119,7 +119,7 @@ with You( ], formats=[ models.ContentsFormats.HTML, models.ContentsFormats.MARKDOWN, - ], crawl_timeout=10) + ], crawl_timeout=10, max_age=86400) # Handle response print(res) @@ -134,7 +134,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.contents.generate(urls=[ @@ -142,7 +142,7 @@ with You( ], formats=[ models.ContentsFormats.HTML, models.ContentsFormats.MARKDOWN, - ], crawl_timeout=10) + ], crawl_timeout=10, max_age=86400) # Handle response print(res) @@ -151,13 +151,14 @@ with You( ### Parameters -| Parameter | Type | Required | Description | Example | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `urls` | List[*str*] | :heavy_minus_sign: | Array of URLs to fetch the contents from. | | -| `formats` | List[[models.ContentsFormats](../../models/contentsformats.md)] | :heavy_minus_sign: | Array of content formats to return. All included formats are returned in the response. Include "metadata" to get JSON-LD and OpenGraph information, if available. | [
"html",
"markdown"
] | -| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. | 10 | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | -| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | +| Parameter | Type | Required | Description | Example | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `urls` | List[*str*] | :heavy_minus_sign: | Array of URLs to fetch the contents from. | | +| `formats` | List[[models.ContentsFormats](../../models/contentsformats.md)] | :heavy_minus_sign: | Array of content formats to return. All included formats are returned in the response. Include "metadata" to get JSON-LD and OpenGraph information, if available. | [
"html",
"markdown"
] | +| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. | 10 | +| `max_age` | *OptionalNullable[int]* | :heavy_minus_sign: | Maximum allowed age of cached content in seconds. When set, cached content older than this threshold is ignored and the page is re-fetched. Must be 0 or greater. Default: null (no age limit, cached content is returned regardless of age). | 86400 | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | +| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | ### Response diff --git a/docs/sdks/runs/README.md b/docs/sdks/runs/README.md index c8d570f..11795dd 100644 --- a/docs/sdks/runs/README.md +++ b/docs/sdks/runs/README.md @@ -26,7 +26,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.agents.runs.create(request={ @@ -57,7 +57,7 @@ from youdotcom import You with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.agents.runs.create(request={ @@ -86,7 +86,7 @@ from youdotcom import You with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.agents.runs.create(request={ @@ -110,7 +110,7 @@ from youdotcom import You with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.agents.runs.create(request={ @@ -134,7 +134,7 @@ from youdotcom import You with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.agents.runs.create(request={ @@ -158,7 +158,7 @@ from youdotcom import You with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: res = you.agents.runs.create(request={ @@ -185,6 +185,7 @@ with You( | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | | `request` | [models.AgentsRunsRequest](../../models/agentsrunsrequest.md) | :heavy_check_mark: | The request object to use for the request. | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | +| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | ### Response diff --git a/docs/sdks/search/README.md b/docs/sdks/search/README.md index 72bfdc7..c87dcec 100644 --- a/docs/sdks/search/README.md +++ b/docs/sdks/search/README.md @@ -10,6 +10,8 @@ This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. +`GET` is a good choice for simple queries where HTTP cacheability matters—GET responses can be cached at CDN and proxy layers, whereas POST responses are not cached by default per the HTTP spec. For requests with complex parameters such as `include_domains` or `exclude_domains`, use POST instead - domain lists are passed as comma-separated strings in GET and are limited by URL length. + ### Example Usage @@ -19,10 +21,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.search.unified(query="Your query", count=10, language=models.Language.EN) + res = you.search.unified(query="Your query", count=10, language=models.Language.EN, include_domains="nytimes.com,bbc.com", exclude_domains="spam-site.com,other-site.com", boost_domains="nytimes.com,wired.com", crawl_timeout=10) # Handle response print(res) @@ -33,15 +35,19 @@ with You( | Parameter | Type | Required | Description | Example | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | Your query | -| `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | -| `freshness` | [Optional[models.SearchFreshness]](../../models/searchfreshness.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | -| `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | -| `country` | [Optional[models.SearchCountry]](../../models/searchcountry.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | +| `query` | *str* | :heavy_check_mark: | N/A | What are the latest geopolitical updates from India | +| `count` | *Optional[int]* | :heavy_minus_sign: | N/A | | +| `freshness` | [Optional[models.FreshnessValue]](../../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | +| `offset` | *Optional[int]* | :heavy_minus_sign: | N/A | | +| `country` | [Optional[models.Country]](../../models/country.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | | `language` | [Optional[models.Language]](../../models/language.md) | :heavy_minus_sign: | The language of the web results that will be returned (BCP 47 format). | | -| `safesearch` | [Optional[models.SearchSafesearch]](../../models/searchsafesearch.md) | :heavy_minus_sign: | Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. | | -| `livecrawl` | [Optional[models.SearchLivecrawl]](../../models/searchlivecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | -| `livecrawl_formats` | [Optional[models.SearchLivecrawlFormats]](../../models/searchlivecrawlformats.md) | :heavy_minus_sign: | Indicates the format of the livecrawled content. | | +| `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. | | +| `livecrawl` | [Optional[models.LiveCrawl]](../../models/livecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | +| `livecrawl_formats` | List[[models.LiveCrawlFormats](../../models/livecrawlformats.md)] | :heavy_minus_sign: | N/A | | +| `include_domains` | *Optional[str]* | :heavy_minus_sign: | A list of domains to restrict search results to. Only results from these domains will be returned. For large domain lists (up to 500), use POST with a JSON array instead. This is a strict allowlist — cannot be combined with `exclude_domains` (returns `422`).

**Important:** Use a single comma-separated value (e.g. `include_domains=nytimes.com,bbc.com`). Repeated parameters (`include_domains=a.com&include_domains=b.com`) are not supported. | nytimes.com,bbc.com | +| `exclude_domains` | *Optional[str]* | :heavy_minus_sign: | A list of domains to exclude from search results. Results from these domains will be filtered out. For large domain lists (up to 500), use POST with a JSON array instead. Cannot be combined with `include_domains` (returns `422`).

**Important:** You must use a single comma-separated value (e.g. `exclude_domains=spam-site.com,other-site.com`). Repeated parameters are not supported. | spam-site.com,other-site.com | +| `boost_domains` | *Optional[str]* | :heavy_minus_sign: | A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`).

**Important:** You must use a single comma-separated value (e.g. `boost_domains=nytimes.com,wired.com`). Repeated parameters are not supported. | nytimes.com,wired.com | +| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | N/A | 10 | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | | `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | @@ -51,9 +57,10 @@ with You( ### Errors -| Error Type | Status Code | Content Type | -| -------------------------------- | -------------------------------- | -------------------------------- | -| errors.SearchUnauthorizedError | 401 | application/json | -| errors.SearchForbiddenError | 403 | application/json | -| errors.SearchInternalServerError | 500 | application/json | -| errors.YouDefaultError | 4XX, 5XX | \*/\* | \ No newline at end of file +| Error Type | Status Code | Content Type | +| --------------------------------------- | --------------------------------------- | --------------------------------------- | +| errors.UnauthorizedResponseError | 401 | application/json | +| errors.ForbiddenResponseError | 403 | application/json | +| errors.UnprocessableEntityResponseError | 422 | application/json | +| errors.InternalServerErrorResponse | 500 | application/json | +| errors.YouDefaultError | 4XX, 5XX | \*/\* | \ No newline at end of file diff --git a/docs/sdks/you/README.md b/docs/sdks/you/README.md index 6be1470..fc31c5f 100644 --- a/docs/sdks/you/README.md +++ b/docs/sdks/you/README.md @@ -2,18 +2,255 @@ ## Overview -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 Get the best search results from web and news sources Returns the HTML or Markdown of a target webpage -Multi-step reasoning with comprehensive research capabilities Comprehensive API for You.com services: - **Agents API**: Execute queries using Express, Advanced, and Custom AI agents +- **Research API**: In-depth, multi-step research with citations and sources +- **Finance Research API**: Finance-focused multi-step research with citations and sources - **Search API**: Get search results from web and news sources - **Contents API**: Retrieve and process web page content +Multi-step reasoning with comprehensive research capabilities +Finance-focused multi-step research with competitive accuracy at same price points and latencies as the Research API ### Available Operations +* [search_post](#search_post) - Returns a list of unified search results from web and news sources * [research](#research) - Returns comprehensive research-grade answers with multi-step reasoning +* [get_research_task](#get_research_task) - Get the status of a background research task +* [stream_research_task](#stream_research_task) - Stream updates for a background research task +* [finance_research](#finance_research) - Returns comprehensive finance-grade research answers with multi-step reasoning + +## search_post + +This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. + +`POST` is the recommended method when using complex parameters such as `include_domains` or `exclude_domains`. These fields accept JSON arrays in the request body, which is unambiguous and supports up to 500 domains per request—something that would exceed URL length limits with GET. Use GET for simple queries where HTTP cacheability matters. + +### Example Usage: authFailure + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, include_domains=[ + "nytimes.com", + "bbc.com", + ], exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: authorizationFailure + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, include_domains=[ + "nytimes.com", + "bbc.com", + ], exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: invalidOrExpired + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, include_domains=[ + "nytimes.com", + "bbc.com", + ], exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: invalidParams + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, include_domains=[ + "nytimes.com", + "bbc.com", + ], exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: missingApiKey + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, include_domains=[ + "nytimes.com", + "bbc.com", + ], exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: missingScopes + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, include_domains=[ + "nytimes.com", + "bbc.com", + ], exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) + + # Handle response + print(res) + +``` +### Example Usage: otherAuthParsing + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, include_domains=[ + "nytimes.com", + "bbc.com", + ], exclude_domains=[ + "spam-site.com", + "other-site.com", + ], boost_domains=[ + "nytimes.com", + "wired.com", + ], crawl_timeout=10) + + # Handle response + print(res) + +``` + +### Parameters + +| Parameter | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | What are the latest geopolitical updates from India | +| `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | +| `freshness` | [Optional[models.FreshnessValue]](../../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | +| `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | +| `country` | [Optional[models.Country]](../../models/country.md) | :heavy_minus_sign: | The country code that determines the geographical focus of the web results. | | +| `language` | [Optional[models.Language]](../../models/language.md) | :heavy_minus_sign: | The language of the web results that will be returned (BCP 47 format). | | +| `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. | | +| `livecrawl` | [Optional[models.LiveCrawl]](../../models/livecrawl.md) | :heavy_minus_sign: | Indicates which section(s) of search results to livecrawl and return full page content. | | +| `livecrawl_formats` | List[[models.LiveCrawlFormats](../../models/livecrawlformats.md)] | :heavy_minus_sign: | Indicates the format(s) of the livecrawled content. Pass one or both values (`html`, `markdown`). In a GET request, repeat the parameter: `?livecrawl_formats=html&livecrawl_formats=markdown`. In a POST body, provide a JSON array: `["html", "markdown"]`. | | +| `include_domains` | List[*str*] | :heavy_minus_sign: | A list of domains to restrict search results to. Only results from these domains will be returned. Supports up to 500 domains. This is a strict allowlist, not a boost — results are limited exclusively to the specified domains.

Cannot be combined with `exclude_domains`; passing both will return a `422` error. | [
"nytimes.com",
"bbc.com"
] | +| `exclude_domains` | List[*str*] | :heavy_minus_sign: | A list of domains to exclude from search results. Results from these domains will be filtered out. Supports up to 500 domains.

Cannot be combined with `include_domains`; passing both will return a `422` error. | [
"spam-site.com",
"other-site.com"
] | +| `boost_domains` | List[*str*] | :heavy_minus_sign: | A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). | [
"nytimes.com",
"wired.com"
] | +| `crawl_timeout` | *Optional[int]* | :heavy_minus_sign: | Maximum time in seconds to wait for page content when `livecrawl` is enabled. Must be between 1 and 60 seconds. Default is 10 seconds. | 10 | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | +| `server_url` | *Optional[str]* | :heavy_minus_sign: | An optional server URL to use. | http://localhost:8080 | + +### Response + +**[models.SearchResponse](../../models/searchresponse.md)** + +### Errors + +| Error Type | Status Code | Content Type | +| --------------------------------------- | --------------------------------------- | --------------------------------------- | +| errors.UnauthorizedResponseError | 401 | application/json | +| errors.ForbiddenResponseError | 403 | application/json | +| errors.UnprocessableEntityResponseError | 422 | application/json | +| errors.InternalServerErrorResponse | 500 | application/json | +| errors.YouDefaultError | 4XX, 5XX | \*/\* | ## research @@ -28,10 +265,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -46,10 +283,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -64,10 +301,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -82,10 +319,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -100,10 +337,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -118,10 +355,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -136,10 +373,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -154,10 +391,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -172,10 +409,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -190,10 +427,10 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YOU_API_KEY_AUTH", ""), + api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) + res = you.research(input="", research_effort=models.ResearchEffort.STANDARD, background=False) # Handle response print(res) @@ -206,18 +443,320 @@ with You( | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `input` | *str* | :heavy_check_mark: | The research question or complex query requiring in-depth investigation and multi-step reasoning.

Note: The maximum length of the input is 40,000 characters. | | `research_effort` | [Optional[models.ResearchEffort]](../../models/researcheffort.md) | :heavy_minus_sign: | Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time.

Available levels:
- `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer.
- `standard`: The default. Balances speed and depth, a good fit for most questions.
- `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed.
- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. | +| `background` | *Optional[bool]* | :heavy_minus_sign: | When true, queue a research task and return a task handle immediately instead of waiting for the result inline. Defaults to synchronous. When enabled, the response is a TaskResponse object with a task_id and stream_url for polling progress via GET /v1/research/{task_id} or streaming via GET /v1/research/{task_id}/stream. | +| `source_control` | [Optional[models.SourceControl]](../../models/sourcecontrol.md) | :heavy_minus_sign: | Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country.

`include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. | +| `output_schema` | [Optional[models.OutputSchema]](../../models/outputschema.md) | :heavy_minus_sign: | Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: "lite" returns 422.

Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported.

Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | ### Response -**[models.ResearchResponse](../../models/researchresponse.md)** +**[models.ResearchResponse1](../../models/researchresponse1.md)** + +### Errors + +| Error Type | Status Code | Content Type | +| --------------------------------------- | --------------------------------------- | --------------------------------------- | +| errors.ResearchUnauthorizedError | 401 | application/json | +| errors.ResearchForbiddenError | 403 | application/json | +| errors.ResearchUnprocessableEntityError | 422 | application/json | +| errors.ResearchInternalServerError | 500 | application/json | +| errors.YouDefaultError | 4XX, 5XX | \*/\* | + +## get_research_task + +Poll the status of a background research task created with background=true. When the task is completed, the result is included in the response. + +### Example Usage + + +```python +import os +from youdotcom import You + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.get_research_task(task_id="586a9bc3-2c52-499c-a61d-be3cc9170c51") + + # Handle response + print(res) + +``` + +### Parameters + +| Parameter | Type | Required | Description | +| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `task_id` | *str* | :heavy_check_mark: | The UUID of the research task. | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | + +### Response + +**[models.TaskDetail](../../models/taskdetail.md)** + +### Errors + +| Error Type | Status Code | Content Type | +| ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | +| errors.GetResearchTaskUnauthorizedError | 401 | application/json | +| errors.GetResearchTaskForbiddenError | 403 | application/json | +| errors.GetResearchTaskNotFoundError | 404 | application/json | +| errors.GetResearchTaskInternalServerError | 500 | application/json | +| errors.YouDefaultError | 4XX, 5XX | \*/\* | + +## stream_research_task + +Stream real-time updates for a background research task via Server-Sent Events (SSE). Supports reconnection via the from_id query parameter to replay missed events. The connection closes automatically when the task reaches a terminal state. + +### Example Usage + + +```python +import os +from youdotcom import You + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.stream_research_task(task_id="b431835b-e51d-453e-a623-25615ac31489", from_id=0) + + with res as event_stream: + for event in event_stream: + # handle event + print(event, flush=True) + +``` + +### Parameters + +| Parameter | Type | Required | Description | +| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `task_id` | *str* | :heavy_check_mark: | The UUID of the research task. | +| `from_id` | *Optional[int]* | :heavy_minus_sign: | Resume from a sequence number for reconnection. | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | + +### Response + +**[Union[eventstreaming.EventStream[models.ResearchTaskStreamEvent], eventstreaming.EventStreamAsync[models.ResearchTaskStreamEvent]]](../../models/.md)** + +### Errors + +| Error Type | Status Code | Content Type | +| -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | +| errors.StreamResearchTaskUnauthorizedError | 401 | application/json | +| errors.StreamResearchTaskForbiddenError | 403 | application/json | +| errors.StreamResearchTaskNotFoundError | 404 | application/json | +| errors.StreamResearchTaskInternalServerError | 500 | application/json | +| errors.YouDefaultError | 4XX, 5XX | \*/\* | + +## finance_research + +The Finance Research API is purpose-built for financial questions. Like the Research API, it runs multiple searches, reads through sources, and synthesizes everything into a thorough, well-cited answer — but its retrieval index is optimized for financial data: earnings reports, SEC filings, analyst coverage, market data, and financial news. +Use it when you need credible, sourced answers to financial questions: company fundamentals, market trends, competitive analysis, earnings summaries, or macroeconomic research. + +### Example Usage: authFailure + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: authorizationFailure + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: invalidEnum + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: invalidJson + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: invalidOrExpired + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: missingApiKey + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: missingField + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: missingScopes + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: otherAuthParsing + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` +### Example Usage: stringTooLong + + +```python +import os +from youdotcom import You, models + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.finance_research(input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", research_effort=models.FinanceResearchEffort.DEEP) + + # Handle response + print(res) + +``` + +### Parameters + +| Parameter | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `input` | *str* | :heavy_check_mark: | The financial research question or complex query requiring in-depth investigation and multi-step reasoning.

Note: The maximum length of the input is 40,000 characters. | What were the key drivers of NVIDIA's revenue growth in fiscal year 2025? | +| `research_effort` | [Optional[models.FinanceResearchEffort]](../../models/financeresearcheffort.md) | :heavy_minus_sign: | Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time.

Available levels:
- `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research.
- `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. | deep | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | | + +### Response + +**[models.FinanceResearchResponse](../../models/financeresearchresponse.md)** ### Errors -| Error Type | Status Code | Content Type | -| ---------------------------------- | ---------------------------------- | ---------------------------------- | -| errors.ResearchUnauthorizedError | 401 | application/json | -| errors.ResearchForbiddenError | 403 | application/json | -| errors.UnprocessableEntityError | 422 | application/json | -| errors.ResearchInternalServerError | 500 | application/json | -| errors.YouDefaultError | 4XX, 5XX | \*/\* | \ No newline at end of file +| Error Type | Status Code | Content Type | +| ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | +| errors.FinanceResearchUnauthorizedError | 401 | application/json | +| errors.FinanceResearchForbiddenError | 403 | application/json | +| errors.FinanceResearchUnprocessableEntityError | 422 | application/json | +| errors.FinanceResearchInternalServerError | 500 | application/json | +| errors.YouDefaultError | 4XX, 5XX | \*/\* | \ No newline at end of file diff --git a/examples/api-example-calls.py b/examples/api-example-calls.py index e5e0807..756d564 100755 --- a/examples/api-example-calls.py +++ b/examples/api-example-calls.py @@ -19,6 +19,7 @@ """ from typing import Optional +import time from youdotcom import You from youdotcom.models import ( ResearchTool, @@ -41,7 +42,10 @@ ContentsFormats, WebSearchTool, ResearchEffort, + FinanceResearchEffort, + FinanceResearchResponse, ResearchResponse, + TaskResponse, ) from youdotcom.utils import eventstreaming @@ -210,7 +214,7 @@ def search_request(): query="artificial intelligence in farming", count=1, livecrawl=LiveCrawl.WEB, - livecrawl_formats=LiveCrawlFormats.MARKDOWN + livecrawl_formats=[LiveCrawlFormats.MARKDOWN] ) print("Metadata:") @@ -284,6 +288,7 @@ def research_request(): assert isinstance(res, ResearchResponse) print("Research Answer:") + # `output.content` is a string when content_type is "text" print(res.output.content[:500] + "..." if len(res.output.content) > 500 else res.output.content) if res.output.sources: @@ -292,6 +297,172 @@ def research_request(): print(f" - {source.title or 'Untitled'}: {source.url}") +def research_background_request(): + """ + Research API with background mode: returns a task handle instead of the final answer. + Poll status with `you.get_research_task(task_id)` or stream events with + `you.stream_research_task(task_id)`. + """ + print("\n🚀 Running Research Background Request...\n") + + assert you is not None, "SDK client not initialized" + + res = you.research( + input="Compare the profitability of NVIDIA, AMD, and Intel over the past 5 fiscal years.", + research_effort=ResearchEffort.DEEP, + background=True, + ) + + assert isinstance(res, TaskResponse) + print(f"Queued task {res.task_id} (status: {res.status.value})") + print(f"Stream URL: {res.stream_url}") + + # Optional: poll until completion + print("\nPolling for completion...") + while True: + status_res = you.get_research_task(task_id=res.task_id) + status = status_res.status.value + print(f" status: {status}") + if status in ("completed", "failed", "cancelled"): + break + time.sleep(5) + + if status == "completed": + # The typed `Result` model has no fields today (the SDK uses + # `extra="ignore"` on the typed result envelope), so `model_dump()` + # cannot recover the inline `ResearchResponse` payload from + # `status_res.result`. Use a synchronous `research(..., background=False)` + # call with the same input to retrieve the typed answer. + print("\nFinal answer (preview):") + sync_res = you.research( + input="Compare the profitability of NVIDIA, AMD, and Intel over the past 5 fiscal years.", + research_effort=ResearchEffort.DEEP, + ) + assert isinstance(sync_res, ResearchResponse) + content = sync_res.output.content + if isinstance(content, str): + print(content[:500] + ("..." if len(content) > 500 else "")) + + +def research_output_schema_request(): + """ + Research API with `output_schema` for structured JSON output. + """ + print("\n🚀 Running Research with output_schema...\n") + + assert you is not None, "SDK client not initialized" + + res = you.research( + input="Are \"Acme Logistics LLC\" (Delaware) and \"Acme Logistics\" (Newark, NJ) the same business?", + research_effort=ResearchEffort.STANDARD, + output_schema={ + "type": "object", + "properties": { + "same_entity": {"type": "boolean"}, + "confidence": {"type": "number"}, + "evidence": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["same_entity", "confidence", "evidence"], + "additionalProperties": False, + }, + ) + + assert isinstance(res, ResearchResponse) + print(f"content_type: {res.output.content_type.value}") + # Caveat (2.4.0): the typed `Content` model declares no fields and + # pydantic's `extra="ignore"` drops the structured payload at + # unmarshal time, so `res.output.content` is an empty `Content()` for + # structured responses today. To retrieve the typed object, re-issue + # the same call synchronously (background=False): + typed_res = you.research( + input="Are \"Acme Logistics LLC\" (Delaware) and \"Acme Logistics\" (Newark, NJ) the same business?", + research_effort=ResearchEffort.STANDARD, + output_schema={ + "type": "object", + "properties": { + "same_entity": {"type": "boolean"}, + "confidence": {"type": "number"}, + "evidence": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["same_entity", "confidence", "evidence"], + "additionalProperties": False, + }, + ) + structured_content = typed_res.output.content + print(f"structured payload: {structured_content}") + + +def finance_research_request(): + """ + Finance Research API endpoint for finance-focused multi-step research. + Returns a Markdown answer with citations from financial sources (SEC filings, + earnings releases, analyst coverage, market data) instead of the open web. + """ + print("\n🚀 Running Finance Research Request...\n") + + assert you is not None, "SDK client not initialized" + + res = you.finance_research( + input="What were the key drivers of NVIDIA's revenue growth in fiscal year 2025?", + research_effort=FinanceResearchEffort.DEEP, + ) + + assert isinstance(res, FinanceResearchResponse) + print("Finance Answer:") + # Finance Research always returns `content` as a Markdown string (content_type: "text"). + print(res.output.content[:500] + "..." if len(res.output.content) > 500 else res.output.content) + + if res.output.sources: + print(f"\nSources ({len(res.output.sources)}):") + for source in res.output.sources: + print(f" - {source.title or 'Untitled'}: {source.url}") + + +def search_request_with_boost(): + """ + Search API: use `boost_domains` to prefer certain domains in ranking + without excluding other domains. Useful when you want sources-with-preference + rather than a strict allow-list (`include_domains`). + """ + print("\n🚀 Running Search Request (boost_domains)...\n") + + assert you is not None, "SDK client not initialized" + + results = you.search.unified( + query="latest advances in fusion energy research", + count=5, + boost_domains="nature.com,science.org,arxiv.org", + ) + + print("Top results:") + if results.results and results.results.web: + for result in results.results.web[:5]: + print(f" - {result.title or 'Untitled'}: {result.url}") + + +def content_request_with_max_age(): + """ + Contents API: use `max_age` to control cache freshness (in seconds). + Pass `max_age=0` to always re-fetch, or e.g. `max_age=86400` to require + cached content less than 24 hours old. + """ + print("\n🚀 Running Content Request (max_age)...\n") + + assert you is not None, "SDK client not initialized" + + results = you.contents.generate( + urls=["https://example.com/page"], + formats=[ContentsFormats.MARKDOWN], + crawl_timeout=20, + max_age=86400, # require cache less than 24 hours old + ) + + for result in results: + print(f" URL: {result.url}") + if result.markdown: + print(f" Markdown preview: {result.markdown[:120]}...") + + # Available functions menu FUNCTIONS = [ {"name": "Express Batch Request", "fn": express_batch_request}, @@ -299,8 +470,13 @@ def research_request(): {"name": "Advanced Batch Request", "fn": advanced_batch_request}, {"name": "Custom Batch Request", "fn": custom_batch_request}, {"name": "Search Request", "fn": search_request}, + {"name": "Search Request (boost_domains)", "fn": search_request_with_boost}, {"name": "Content Request", "fn": content_request}, + {"name": "Content Request (max_age)", "fn": content_request_with_max_age}, {"name": "Research Request", "fn": research_request}, + {"name": "Research Background Mode", "fn": research_background_request}, + {"name": "Research with output_schema", "fn": research_output_schema_request}, + {"name": "Finance Research Request", "fn": finance_research_request}, ] diff --git a/overlays/python_overlay.yaml b/overlays/python_overlay.yaml index cfbe9a4..ddd81d0 100644 --- a/overlays/python_overlay.yaml +++ b/overlays/python_overlay.yaml @@ -2,8 +2,13 @@ overlay: 1.0.0 x-speakeasy-jsonpath: rfc9535 info: title: Python Specific Modifications - version: 1.0.0 + version: 1.1.0 actions: + # Restore the top-level spec title — the frontend spec changed info.title + # to "You.com Finance Research API" which mislabels the entire SDK in the + # regenerated README summary. Keep it as "You.com API". + - target: $["info"]["title"] + update: "You.com API" - target: $["paths"]["/v1/contents"]["post"] update: tags: @@ -19,3 +24,16 @@ actions: tags: - agents.runs x-speakeasy-name-override: create + # Allow extras on the anonymous object branch of `output.content` so + # `output_schema=` requests preserve the structured payload through SDK + # unmarshal (today: `class Content(BaseModel): pass` drops everything + # due to pydantic `extra="ignore"`). Reviewed as part of Step 4i-9. + - target: $["components"]["schemas"]["ResearchResponse"]["properties"]["output"]["properties"]["content"]["oneOf"][1] + update: + additionalProperties: true + # Same fix for `TaskDetail.result` so background-mode research surfaces + # the typed `ResearchResponse` payload (today: `class Result(BaseModel): pass` + # round-trips as an empty dict). + - target: $["components"]["schemas"]["TaskDetail"]["properties"]["result"] + update: + additionalProperties: true diff --git a/pyproject.toml b/pyproject.toml index f0e24df..d2ce60e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "youdotcom" -version = "2.3.0" +version = "2.4.0" description = "The official You.com Python SDK." authors = [{ name = "You.com" },] readme = "README.md" @@ -8,7 +8,7 @@ requires-python = ">=3.10" dependencies = [ "httpcore >=1.0.9", "httpx >=0.28.1", - "pydantic >=2.11.2", + "pydantic >=2.11.2,<2.13", ] license = { text = "Apache-2.0" } diff --git a/src/youdotcom/_hooks/registration.py b/src/youdotcom/_hooks/registration.py index 66530ec..8898de6 100644 --- a/src/youdotcom/_hooks/registration.py +++ b/src/youdotcom/_hooks/registration.py @@ -1,32 +1,36 @@ from .types import Hooks, BeforeRequestHook, BeforeRequestContext import httpx from typing import Union +from .._version import __user_agent__ # This file is only ever generated once on the first generation and then is free to be modified. # Any hooks you wish to add should be registered in the init_hooks function. Feel free to define them # in this file or in separate files in the hooks folder. +_DEFAULT_UA_PREFIX = "speakeasy-sdk/" + + class YDCUserAgentOverrideHook(BeforeRequestHook): - """Hook that overrides the User-Agent header in all requests with browser fallback.""" + """Hook that overrides the User-Agent header on every request. - def before_request(self, hook_ctx: BeforeRequestContext, request: httpx.Request) -> Union[httpx.Request, Exception]: - """ - Override the User-Agent header before the request is sent. + Behaviour: + - If ``sdk_configuration.user_agent`` has been overridden away from the + speakeasy-default (``speakeasy-sdk/python ...``), pass it through so + integrations (langchain-youdotcom, youdotcom-temporal, + n8n-nodes-youdotcom) can identify their traffic. + - Otherwise, emit the SDK-default ``youdotcom-python-sdk/{sdk_version}``. + """ - In browser environments where setting User-Agent may be restricted, - this hook falls back to using the x-sdk-user-agent custom header. - """ + def before_request(self, hook_ctx: BeforeRequestContext, request: httpx.Request) -> Union[httpx.Request, Exception]: sdk_version = hook_ctx.config.sdk_version - user_agent = f"youdotcom-python-sdk/{sdk_version}" + configured_ua = hook_ctx.config.user_agent - # Try to set the standard User-Agent header first - request.headers["User-Agent"] = user_agent + is_custom = bool(configured_ua) and configured_ua != __user_agent__ and not configured_ua.startswith(_DEFAULT_UA_PREFIX) - # Check if the header was actually set - if not request.headers.get("User-Agent"): - # Fall back to a custom header if the User-Agent couldn't be set - request.headers["x-sdk-user-agent"] = user_agent + request.headers["User-Agent"] = ( + configured_ua if is_custom else f"youdotcom-python-sdk/{sdk_version}" + ) return request diff --git a/src/youdotcom/_hooks/types.py b/src/youdotcom/_hooks/types.py index 13987ce..2b03ad3 100644 --- a/src/youdotcom/_hooks/types.py +++ b/src/youdotcom/_hooks/types.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod import httpx -from typing import Any, Callable, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from youdotcom.sdkconfiguration import SDKConfiguration @@ -12,6 +12,8 @@ class HookContext: operation_id: str oauth2_scopes: Optional[List[str]] = None security_source: Optional[Union[Any, Callable[[], Any]]] = None + tags: Optional[List[str]] = None + extensions: Optional[Dict[str, Any]] = None def __init__( self, @@ -20,12 +22,16 @@ def __init__( operation_id: str, oauth2_scopes: Optional[List[str]], security_source: Optional[Union[Any, Callable[[], Any]]], + tags: Optional[List[str]], + extensions: Optional[Dict[str, Any]], ): self.config = config self.base_url = base_url self.operation_id = operation_id self.oauth2_scopes = oauth2_scopes self.security_source = security_source + self.tags = tags + self.extensions = extensions class BeforeRequestContext(HookContext): @@ -36,6 +42,8 @@ def __init__(self, hook_ctx: HookContext): hook_ctx.operation_id, hook_ctx.oauth2_scopes, hook_ctx.security_source, + hook_ctx.tags, + hook_ctx.extensions, ) @@ -47,6 +55,8 @@ def __init__(self, hook_ctx: HookContext): hook_ctx.operation_id, hook_ctx.oauth2_scopes, hook_ctx.security_source, + hook_ctx.tags, + hook_ctx.extensions, ) @@ -58,6 +68,8 @@ def __init__(self, hook_ctx: HookContext): hook_ctx.operation_id, hook_ctx.oauth2_scopes, hook_ctx.security_source, + hook_ctx.tags, + hook_ctx.extensions, ) diff --git a/src/youdotcom/_version.py b/src/youdotcom/_version.py index c998bfd..0b3ec3f 100644 --- a/src/youdotcom/_version.py +++ b/src/youdotcom/_version.py @@ -3,10 +3,10 @@ import importlib.metadata __title__: str = "youdotcom" -__version__: str = "2.3.0" -__openapi_doc_version__: str = "1.0.0" -__gen_version__: str = "2.845.12" -__user_agent__: str = "speakeasy-sdk/python 2.3.0 2.845.12 1.0.0 youdotcom" +__version__: str = "2.4.0" +__openapi_doc_version__: str = "0.0.1" +__gen_version__: str = "2.916.4" +__user_agent__: str = "speakeasy-sdk/python 2.4.0 2.916.4 0.0.1 youdotcom" try: if __package__ is not None: diff --git a/src/youdotcom/basesdk.py b/src/youdotcom/basesdk.py index ea145dd..8665d7f 100644 --- a/src/youdotcom/basesdk.py +++ b/src/youdotcom/basesdk.py @@ -9,6 +9,7 @@ AfterErrorContext, AfterSuccessContext, BeforeRequestContext, + HookContext, ) from youdotcom.utils import ( RetryConfig, @@ -66,6 +67,7 @@ def _build_request_async( url_override: Optional[str] = None, http_headers: Optional[Mapping[str, str]] = None, allow_empty_value: Optional[List[str]] = None, + allowed_fields: Optional[List[str]] = None, ) -> httpx.Request: client = self.sdk_configuration.async_client return self._build_request_with_client( @@ -87,6 +89,7 @@ def _build_request_async( url_override, http_headers, allow_empty_value, + allowed_fields, ) def _build_request( @@ -110,6 +113,7 @@ def _build_request( url_override: Optional[str] = None, http_headers: Optional[Mapping[str, str]] = None, allow_empty_value: Optional[List[str]] = None, + allowed_fields: Optional[List[str]] = None, ) -> httpx.Request: client = self.sdk_configuration.client return self._build_request_with_client( @@ -131,6 +135,7 @@ def _build_request( url_override, http_headers, allow_empty_value, + allowed_fields, ) def _build_request_with_client( @@ -155,6 +160,7 @@ def _build_request_with_client( url_override: Optional[str] = None, http_headers: Optional[Mapping[str, str]] = None, allow_empty_value: Optional[List[str]] = None, + allowed_fields: Optional[List[str]] = None, ) -> httpx.Request: query_params = {} @@ -188,7 +194,9 @@ def _build_request_with_client( security = security() security = utils.get_security_from_env(security, models.Security) if security is not None: - security_headers, security_query_params = utils.get_security(security) + security_headers, security_query_params = utils.get_security( + security, allowed_fields + ) headers = {**headers, **security_headers} query_params = {**query_params, **security_query_params} @@ -225,15 +233,15 @@ def _build_request_with_client( data=serialized_request_body.data, files=serialized_request_body.files, headers=headers, - timeout=timeout, + timeout=timeout if timeout is not None else httpx.USE_CLIENT_DEFAULT, ) def do_request( self, - hook_ctx, - request, - error_status_codes, - stream=False, + hook_ctx: HookContext, + request: httpx.Request, + is_error_status_code: Callable[[int], bool], + stream: bool = False, retry_config: Optional[Tuple[RetryConfig, List[str]]] = None, ) -> httpx.Response: client = self.sdk_configuration.client @@ -245,6 +253,8 @@ def do(): http_res = None try: req = hooks.before_request(BeforeRequestContext(hook_ctx), request) + if "timeout" in request.extensions and "timeout" not in req.extensions: + req.extensions["timeout"] = request.extensions["timeout"] logger.debug( "Request:\nMethod: %s\nURL: %s\nHeaders: %s\nBody: %s", req.method, @@ -275,19 +285,6 @@ def do(): "" if stream else http_res.text, ) - if utils.match_status_codes(error_status_codes, http_res.status_code): - result, err = hooks.after_error( - AfterErrorContext(hook_ctx), http_res, None - ) - if err is not None: - logger.debug("Request Exception", exc_info=True) - raise err - if result is not None: - http_res = result - else: - logger.debug("Raising unexpected SDK error") - raise errors.YouDefaultError("Unexpected error occurred", http_res) - return http_res if retry_config is not None: @@ -295,17 +292,27 @@ def do(): else: http_res = do() - if not utils.match_status_codes(error_status_codes, http_res.status_code): + if is_error_status_code(http_res.status_code): + result, err = hooks.after_error(AfterErrorContext(hook_ctx), http_res, None) + if err is not None: + logger.debug("Request Exception", exc_info=True) + raise err + if result is not None: + http_res = result + else: + logger.debug("Raising unexpected SDK error") + raise errors.YouDefaultError("Unexpected error occurred", http_res) + else: http_res = hooks.after_success(AfterSuccessContext(hook_ctx), http_res) return http_res async def do_request_async( self, - hook_ctx, - request, - error_status_codes, - stream=False, + hook_ctx: HookContext, + request: httpx.Request, + is_error_status_code: Callable[[int], bool], + stream: bool = False, retry_config: Optional[Tuple[RetryConfig, List[str]]] = None, ) -> httpx.Response: client = self.sdk_configuration.async_client @@ -320,6 +327,8 @@ async def do(): hooks.before_request, BeforeRequestContext(hook_ctx), request ) + if "timeout" in request.extensions and "timeout" not in req.extensions: + req.extensions["timeout"] = request.extensions["timeout"] logger.debug( "Request:\nMethod: %s\nURL: %s\nHeaders: %s\nBody: %s", req.method, @@ -353,20 +362,6 @@ async def do(): "" if stream else http_res.text, ) - if utils.match_status_codes(error_status_codes, http_res.status_code): - result, err = await run_sync_in_thread( - hooks.after_error, AfterErrorContext(hook_ctx), http_res, None - ) - - if err is not None: - logger.debug("Request Exception", exc_info=True) - raise err - if result is not None: - http_res = result - else: - logger.debug("Raising unexpected SDK error") - raise errors.YouDefaultError("Unexpected error occurred", http_res) - return http_res if retry_config is not None: @@ -376,7 +371,20 @@ async def do(): else: http_res = await do() - if not utils.match_status_codes(error_status_codes, http_res.status_code): + if is_error_status_code(http_res.status_code): + result, err = await run_sync_in_thread( + hooks.after_error, AfterErrorContext(hook_ctx), http_res, None + ) + + if err is not None: + logger.debug("Request Exception", exc_info=True) + raise err + if result is not None: + http_res = result + else: + logger.debug("Raising unexpected SDK error") + raise errors.YouDefaultError("Unexpected error occurred", http_res) + else: http_res = await run_sync_in_thread( hooks.after_success, AfterSuccessContext(hook_ctx), http_res ) diff --git a/src/youdotcom/contents_sdk.py b/src/youdotcom/contents_sdk.py index a21a07c..e7ce613 100644 --- a/src/youdotcom/contents_sdk.py +++ b/src/youdotcom/contents_sdk.py @@ -1,7 +1,7 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" from .basesdk import BaseSDK -from typing import Any, List, Mapping, Optional +from typing import Any, Iterable, List, Mapping, Optional from youdotcom import errors, models, utils from youdotcom._hooks import HookContext from youdotcom.types import OptionalNullable, UNSET @@ -13,9 +13,10 @@ class ContentsSDK(BaseSDK): def generate( self, *, - urls: Optional[List[str]] = None, - formats: Optional[List[models.ContentsFormats]] = None, + urls: Optional[Iterable[str]] = None, + formats: Optional[Iterable[models.ContentsFormats]] = None, crawl_timeout: Optional[int] = 10, + max_age: OptionalNullable[int] = None, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, @@ -28,6 +29,7 @@ def generate( :param urls: Array of URLs to fetch the contents from. :param formats: Array of content formats to return. All included formats are returned in the response. Include \"metadata\" to get JSON-LD and OpenGraph information, if available. :param crawl_timeout: Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. + :param max_age: Maximum allowed age of cached content in seconds. When set, cached content older than this threshold is ignored and the page is re-fetched. Must be 0 or greater. Default: null (no age limit, cached content is returned regardless of age). :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -44,9 +46,10 @@ def generate( base_url = models.CONTENTS_OP_SERVERS[0] request = models.ContentsRequest( - urls=urls, - formats=formats, + urls=utils.unmarshal(urls, Optional[List[str]]), + formats=utils.unmarshal(formats, Optional[List[models.ContentsFormats]]), crawl_timeout=crawl_timeout, + max_age=max_age, ) req = self._build_request( @@ -86,9 +89,11 @@ def generate( security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), + tags=["contents"], + extensions=None, ), request=req, - error_status_codes=["401", "403", "4XX", "500", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -122,9 +127,10 @@ def generate( async def generate_async( self, *, - urls: Optional[List[str]] = None, - formats: Optional[List[models.ContentsFormats]] = None, + urls: Optional[Iterable[str]] = None, + formats: Optional[Iterable[models.ContentsFormats]] = None, crawl_timeout: Optional[int] = 10, + max_age: OptionalNullable[int] = None, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, @@ -137,6 +143,7 @@ async def generate_async( :param urls: Array of URLs to fetch the contents from. :param formats: Array of content formats to return. All included formats are returned in the response. Include \"metadata\" to get JSON-LD and OpenGraph information, if available. :param crawl_timeout: Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds. + :param max_age: Maximum allowed age of cached content in seconds. When set, cached content older than this threshold is ignored and the page is re-fetched. Must be 0 or greater. Default: null (no age limit, cached content is returned regardless of age). :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -153,9 +160,10 @@ async def generate_async( base_url = models.CONTENTS_OP_SERVERS[0] request = models.ContentsRequest( - urls=urls, - formats=formats, + urls=utils.unmarshal(urls, Optional[List[str]]), + formats=utils.unmarshal(formats, Optional[List[models.ContentsFormats]]), crawl_timeout=crawl_timeout, + max_age=max_age, ) req = self._build_request_async( @@ -195,9 +203,11 @@ async def generate_async( security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), + tags=["contents"], + extensions=None, ), request=req, - error_status_codes=["401", "403", "4XX", "500", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) diff --git a/src/youdotcom/errors/__init__.py b/src/youdotcom/errors/__init__.py index 2193daa..aea2e79 100644 --- a/src/youdotcom/errors/__init__.py +++ b/src/youdotcom/errors/__init__.py @@ -26,6 +26,34 @@ ContentsUnauthorizedError, ContentsUnauthorizedErrorData, ) + from .finance_researchop import ( + FinanceResearchForbiddenError, + FinanceResearchForbiddenErrorData, + FinanceResearchInternalServerError, + FinanceResearchInternalServerErrorData, + FinanceResearchUnauthorizedError, + FinanceResearchUnauthorizedErrorData, + FinanceResearchUnprocessableEntityError, + FinanceResearchUnprocessableEntityErrorData, + ) + from .forbidden_response_error import ( + ForbiddenResponseError, + ForbiddenResponseErrorData, + ) + from .getresearchtaskop import ( + GetResearchTaskForbiddenError, + GetResearchTaskForbiddenErrorData, + GetResearchTaskInternalServerError, + GetResearchTaskInternalServerErrorData, + GetResearchTaskNotFoundError, + GetResearchTaskNotFoundErrorData, + GetResearchTaskUnauthorizedError, + GetResearchTaskUnauthorizedErrorData, + ) + from .internalservererror_response import ( + InternalServerErrorResponse, + InternalServerErrorResponseData, + ) from .no_response_error import NoResponseError from .researchop import ( ResearchForbiddenError, @@ -34,17 +62,27 @@ ResearchInternalServerErrorData, ResearchUnauthorizedError, ResearchUnauthorizedErrorData, - UnprocessableEntityError, - UnprocessableEntityErrorData, + ResearchUnprocessableEntityError, + ResearchUnprocessableEntityErrorData, ) from .responsevalidationerror import ResponseValidationError - from .searchop import ( - SearchForbiddenError, - SearchForbiddenErrorData, - SearchInternalServerError, - SearchInternalServerErrorData, - SearchUnauthorizedError, - SearchUnauthorizedErrorData, + from .streamresearchtaskop import ( + StreamResearchTaskForbiddenError, + StreamResearchTaskForbiddenErrorData, + StreamResearchTaskInternalServerError, + StreamResearchTaskInternalServerErrorData, + StreamResearchTaskNotFoundError, + StreamResearchTaskNotFoundErrorData, + StreamResearchTaskUnauthorizedError, + StreamResearchTaskUnauthorizedErrorData, + ) + from .unauthorized_response_error import ( + UnauthorizedResponseError, + UnauthorizedResponseErrorData, + ) + from .unprocessableentity_response_error import ( + UnprocessableEntityResponseError, + UnprocessableEntityResponseErrorData, ) from .youdefaulterror import YouDefaultError @@ -61,6 +99,26 @@ "ContentsInternalServerErrorData", "ContentsUnauthorizedError", "ContentsUnauthorizedErrorData", + "FinanceResearchForbiddenError", + "FinanceResearchForbiddenErrorData", + "FinanceResearchInternalServerError", + "FinanceResearchInternalServerErrorData", + "FinanceResearchUnauthorizedError", + "FinanceResearchUnauthorizedErrorData", + "FinanceResearchUnprocessableEntityError", + "FinanceResearchUnprocessableEntityErrorData", + "ForbiddenResponseError", + "ForbiddenResponseErrorData", + "GetResearchTaskForbiddenError", + "GetResearchTaskForbiddenErrorData", + "GetResearchTaskInternalServerError", + "GetResearchTaskInternalServerErrorData", + "GetResearchTaskNotFoundError", + "GetResearchTaskNotFoundErrorData", + "GetResearchTaskUnauthorizedError", + "GetResearchTaskUnauthorizedErrorData", + "InternalServerErrorResponse", + "InternalServerErrorResponseData", "NoResponseError", "ResearchForbiddenError", "ResearchForbiddenErrorData", @@ -68,15 +126,21 @@ "ResearchInternalServerErrorData", "ResearchUnauthorizedError", "ResearchUnauthorizedErrorData", + "ResearchUnprocessableEntityError", + "ResearchUnprocessableEntityErrorData", "ResponseValidationError", - "SearchForbiddenError", - "SearchForbiddenErrorData", - "SearchInternalServerError", - "SearchInternalServerErrorData", - "SearchUnauthorizedError", - "SearchUnauthorizedErrorData", - "UnprocessableEntityError", - "UnprocessableEntityErrorData", + "StreamResearchTaskForbiddenError", + "StreamResearchTaskForbiddenErrorData", + "StreamResearchTaskInternalServerError", + "StreamResearchTaskInternalServerErrorData", + "StreamResearchTaskNotFoundError", + "StreamResearchTaskNotFoundErrorData", + "StreamResearchTaskUnauthorizedError", + "StreamResearchTaskUnauthorizedErrorData", + "UnauthorizedResponseError", + "UnauthorizedResponseErrorData", + "UnprocessableEntityResponseError", + "UnprocessableEntityResponseErrorData", "YouDefaultError", "YouError", ] @@ -94,6 +158,26 @@ "ContentsInternalServerErrorData": ".contentsop", "ContentsUnauthorizedError": ".contentsop", "ContentsUnauthorizedErrorData": ".contentsop", + "FinanceResearchForbiddenError": ".finance_researchop", + "FinanceResearchForbiddenErrorData": ".finance_researchop", + "FinanceResearchInternalServerError": ".finance_researchop", + "FinanceResearchInternalServerErrorData": ".finance_researchop", + "FinanceResearchUnauthorizedError": ".finance_researchop", + "FinanceResearchUnauthorizedErrorData": ".finance_researchop", + "FinanceResearchUnprocessableEntityError": ".finance_researchop", + "FinanceResearchUnprocessableEntityErrorData": ".finance_researchop", + "ForbiddenResponseError": ".forbidden_response_error", + "ForbiddenResponseErrorData": ".forbidden_response_error", + "GetResearchTaskForbiddenError": ".getresearchtaskop", + "GetResearchTaskForbiddenErrorData": ".getresearchtaskop", + "GetResearchTaskInternalServerError": ".getresearchtaskop", + "GetResearchTaskInternalServerErrorData": ".getresearchtaskop", + "GetResearchTaskNotFoundError": ".getresearchtaskop", + "GetResearchTaskNotFoundErrorData": ".getresearchtaskop", + "GetResearchTaskUnauthorizedError": ".getresearchtaskop", + "GetResearchTaskUnauthorizedErrorData": ".getresearchtaskop", + "InternalServerErrorResponse": ".internalservererror_response", + "InternalServerErrorResponseData": ".internalservererror_response", "NoResponseError": ".no_response_error", "ResearchForbiddenError": ".researchop", "ResearchForbiddenErrorData": ".researchop", @@ -101,15 +185,21 @@ "ResearchInternalServerErrorData": ".researchop", "ResearchUnauthorizedError": ".researchop", "ResearchUnauthorizedErrorData": ".researchop", - "UnprocessableEntityError": ".researchop", - "UnprocessableEntityErrorData": ".researchop", + "ResearchUnprocessableEntityError": ".researchop", + "ResearchUnprocessableEntityErrorData": ".researchop", "ResponseValidationError": ".responsevalidationerror", - "SearchForbiddenError": ".searchop", - "SearchForbiddenErrorData": ".searchop", - "SearchInternalServerError": ".searchop", - "SearchInternalServerErrorData": ".searchop", - "SearchUnauthorizedError": ".searchop", - "SearchUnauthorizedErrorData": ".searchop", + "StreamResearchTaskForbiddenError": ".streamresearchtaskop", + "StreamResearchTaskForbiddenErrorData": ".streamresearchtaskop", + "StreamResearchTaskInternalServerError": ".streamresearchtaskop", + "StreamResearchTaskInternalServerErrorData": ".streamresearchtaskop", + "StreamResearchTaskNotFoundError": ".streamresearchtaskop", + "StreamResearchTaskNotFoundErrorData": ".streamresearchtaskop", + "StreamResearchTaskUnauthorizedError": ".streamresearchtaskop", + "StreamResearchTaskUnauthorizedErrorData": ".streamresearchtaskop", + "UnauthorizedResponseError": ".unauthorized_response_error", + "UnauthorizedResponseErrorData": ".unauthorized_response_error", + "UnprocessableEntityResponseError": ".unprocessableentity_response_error", + "UnprocessableEntityResponseErrorData": ".unprocessableentity_response_error", "YouDefaultError": ".youdefaulterror", } diff --git a/src/youdotcom/errors/finance_researchop.py b/src/youdotcom/errors/finance_researchop.py new file mode 100644 index 0000000..a42f34d --- /dev/null +++ b/src/youdotcom/errors/finance_researchop.py @@ -0,0 +1,94 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from dataclasses import dataclass, field +import httpx +from typing import List, Optional +from youdotcom.errors import YouError +from youdotcom.models import finance_researchop as models_finance_researchop +from youdotcom.types import BaseModel + + +class FinanceResearchInternalServerErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class FinanceResearchInternalServerError(YouError): + r"""Internal Server Error during authentication/authorization middleware.""" + + data: FinanceResearchInternalServerErrorData = field(hash=False) + + def __init__( + self, + data: FinanceResearchInternalServerErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class FinanceResearchUnprocessableEntityErrorData(BaseModel): + detail: Optional[List[models_finance_researchop.FinanceResearchDetail]] = None + + +@dataclass(unsafe_hash=True) +class FinanceResearchUnprocessableEntityError(YouError): + r"""Unprocessable Entity. Request validation failed.""" + + data: FinanceResearchUnprocessableEntityErrorData = field(hash=False) + + def __init__( + self, + data: FinanceResearchUnprocessableEntityErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class FinanceResearchForbiddenErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class FinanceResearchForbiddenError(YouError): + r"""Forbidden. API key lacks scope for this path.""" + + data: FinanceResearchForbiddenErrorData = field(hash=False) + + def __init__( + self, + data: FinanceResearchForbiddenErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class FinanceResearchUnauthorizedErrorData(BaseModel): + detail: Optional[str] = None + r"""Error detail message.""" + + +@dataclass(unsafe_hash=True) +class FinanceResearchUnauthorizedError(YouError): + r"""Unauthorized. Problems with API key.""" + + data: FinanceResearchUnauthorizedErrorData = field(hash=False) + + def __init__( + self, + data: FinanceResearchUnauthorizedErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) diff --git a/src/youdotcom/errors/forbidden_response_error.py b/src/youdotcom/errors/forbidden_response_error.py new file mode 100644 index 0000000..575dded --- /dev/null +++ b/src/youdotcom/errors/forbidden_response_error.py @@ -0,0 +1,29 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from dataclasses import dataclass, field +import httpx +from typing import Optional +from youdotcom.errors import YouError +from youdotcom.types import BaseModel + + +class ForbiddenResponseErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class ForbiddenResponseError(YouError): + r"""Forbidden. API key lacks scope for this path.""" + + data: ForbiddenResponseErrorData = field(hash=False) + + def __init__( + self, + data: ForbiddenResponseErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) diff --git a/src/youdotcom/errors/searchop.py b/src/youdotcom/errors/getresearchtaskop.py similarity index 51% rename from src/youdotcom/errors/searchop.py rename to src/youdotcom/errors/getresearchtaskop.py index 9bb3794..1c3cda1 100644 --- a/src/youdotcom/errors/searchop.py +++ b/src/youdotcom/errors/getresearchtaskop.py @@ -8,19 +8,19 @@ from youdotcom.types import BaseModel -class SearchInternalServerErrorData(BaseModel): +class GetResearchTaskInternalServerErrorData(BaseModel): detail: Optional[str] = None @dataclass(unsafe_hash=True) -class SearchInternalServerError(YouError): - r"""Internal Server Error during authentication/authorization middleware.""" +class GetResearchTaskInternalServerError(YouError): + r"""Internal Server Error.""" - data: SearchInternalServerErrorData = field(hash=False) + data: GetResearchTaskInternalServerErrorData = field(hash=False) def __init__( self, - data: SearchInternalServerErrorData, + data: GetResearchTaskInternalServerErrorData, raw_response: httpx.Response, body: Optional[str] = None, ): @@ -29,19 +29,40 @@ def __init__( object.__setattr__(self, "data", data) -class SearchForbiddenErrorData(BaseModel): +class GetResearchTaskNotFoundErrorData(BaseModel): detail: Optional[str] = None @dataclass(unsafe_hash=True) -class SearchForbiddenError(YouError): +class GetResearchTaskNotFoundError(YouError): + r"""Task not found or not authorized.""" + + data: GetResearchTaskNotFoundErrorData = field(hash=False) + + def __init__( + self, + data: GetResearchTaskNotFoundErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class GetResearchTaskForbiddenErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class GetResearchTaskForbiddenError(YouError): r"""Forbidden. API key lacks scope for this path.""" - data: SearchForbiddenErrorData = field(hash=False) + data: GetResearchTaskForbiddenErrorData = field(hash=False) def __init__( self, - data: SearchForbiddenErrorData, + data: GetResearchTaskForbiddenErrorData, raw_response: httpx.Response, body: Optional[str] = None, ): @@ -50,20 +71,19 @@ def __init__( object.__setattr__(self, "data", data) -class SearchUnauthorizedErrorData(BaseModel): +class GetResearchTaskUnauthorizedErrorData(BaseModel): detail: Optional[str] = None - r"""Error detail message.""" @dataclass(unsafe_hash=True) -class SearchUnauthorizedError(YouError): +class GetResearchTaskUnauthorizedError(YouError): r"""Unauthorized. Problems with API key.""" - data: SearchUnauthorizedErrorData = field(hash=False) + data: GetResearchTaskUnauthorizedErrorData = field(hash=False) def __init__( self, - data: SearchUnauthorizedErrorData, + data: GetResearchTaskUnauthorizedErrorData, raw_response: httpx.Response, body: Optional[str] = None, ): diff --git a/src/youdotcom/errors/internalservererror_response.py b/src/youdotcom/errors/internalservererror_response.py new file mode 100644 index 0000000..4a8c1a8 --- /dev/null +++ b/src/youdotcom/errors/internalservererror_response.py @@ -0,0 +1,29 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from dataclasses import dataclass, field +import httpx +from typing import Optional +from youdotcom.errors import YouError +from youdotcom.types import BaseModel + + +class InternalServerErrorResponseData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class InternalServerErrorResponse(YouError): + r"""Internal Server Error during authentication/authorization middleware.""" + + data: InternalServerErrorResponseData = field(hash=False) + + def __init__( + self, + data: InternalServerErrorResponseData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) diff --git a/src/youdotcom/errors/researchop.py b/src/youdotcom/errors/researchop.py index b1a5836..a64bebb 100644 --- a/src/youdotcom/errors/researchop.py +++ b/src/youdotcom/errors/researchop.py @@ -30,19 +30,19 @@ def __init__( object.__setattr__(self, "data", data) -class UnprocessableEntityErrorData(BaseModel): +class ResearchUnprocessableEntityErrorData(BaseModel): detail: Optional[List[models_researchop.ResearchDetail]] = None @dataclass(unsafe_hash=True) -class UnprocessableEntityError(YouError): +class ResearchUnprocessableEntityError(YouError): r"""Unprocessable Entity. Request validation failed.""" - data: UnprocessableEntityErrorData = field(hash=False) + data: ResearchUnprocessableEntityErrorData = field(hash=False) def __init__( self, - data: UnprocessableEntityErrorData, + data: ResearchUnprocessableEntityErrorData, raw_response: httpx.Response, body: Optional[str] = None, ): diff --git a/src/youdotcom/errors/streamresearchtaskop.py b/src/youdotcom/errors/streamresearchtaskop.py new file mode 100644 index 0000000..a2ee4a0 --- /dev/null +++ b/src/youdotcom/errors/streamresearchtaskop.py @@ -0,0 +1,92 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from dataclasses import dataclass, field +import httpx +from typing import Optional +from youdotcom.errors import YouError +from youdotcom.types import BaseModel + + +class StreamResearchTaskInternalServerErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class StreamResearchTaskInternalServerError(YouError): + r"""Internal Server Error.""" + + data: StreamResearchTaskInternalServerErrorData = field(hash=False) + + def __init__( + self, + data: StreamResearchTaskInternalServerErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class StreamResearchTaskNotFoundErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class StreamResearchTaskNotFoundError(YouError): + r"""Task not found or not authorized.""" + + data: StreamResearchTaskNotFoundErrorData = field(hash=False) + + def __init__( + self, + data: StreamResearchTaskNotFoundErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class StreamResearchTaskForbiddenErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class StreamResearchTaskForbiddenError(YouError): + r"""Forbidden. API key lacks scope for this path.""" + + data: StreamResearchTaskForbiddenErrorData = field(hash=False) + + def __init__( + self, + data: StreamResearchTaskForbiddenErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) + + +class StreamResearchTaskUnauthorizedErrorData(BaseModel): + detail: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class StreamResearchTaskUnauthorizedError(YouError): + r"""Unauthorized. Problems with API key.""" + + data: StreamResearchTaskUnauthorizedErrorData = field(hash=False) + + def __init__( + self, + data: StreamResearchTaskUnauthorizedErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) diff --git a/src/youdotcom/errors/unauthorized_response_error.py b/src/youdotcom/errors/unauthorized_response_error.py new file mode 100644 index 0000000..dbc2f9f --- /dev/null +++ b/src/youdotcom/errors/unauthorized_response_error.py @@ -0,0 +1,30 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from dataclasses import dataclass, field +import httpx +from typing import Optional +from youdotcom.errors import YouError +from youdotcom.types import BaseModel + + +class UnauthorizedResponseErrorData(BaseModel): + detail: Optional[str] = None + r"""Error detail message.""" + + +@dataclass(unsafe_hash=True) +class UnauthorizedResponseError(YouError): + r"""Unauthorized. Problems with API key.""" + + data: UnauthorizedResponseErrorData = field(hash=False) + + def __init__( + self, + data: UnauthorizedResponseErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) diff --git a/src/youdotcom/errors/unprocessableentity_response_error.py b/src/youdotcom/errors/unprocessableentity_response_error.py new file mode 100644 index 0000000..f915365 --- /dev/null +++ b/src/youdotcom/errors/unprocessableentity_response_error.py @@ -0,0 +1,29 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from dataclasses import dataclass, field +import httpx +from typing import Optional +from youdotcom.errors import YouError +from youdotcom.types import BaseModel + + +class UnprocessableEntityResponseErrorData(BaseModel): + error: Optional[str] = None + + +@dataclass(unsafe_hash=True) +class UnprocessableEntityResponseError(YouError): + r"""Unprocessable Entity. Invalid request parameter combination.""" + + data: UnprocessableEntityResponseErrorData = field(hash=False) + + def __init__( + self, + data: UnprocessableEntityResponseErrorData, + raw_response: httpx.Response, + body: Optional[str] = None, + ): + message = body or raw_response.text + super().__init__(message, raw_response, body) + object.__setattr__(self, "data", data) diff --git a/src/youdotcom/models/__init__.py b/src/youdotcom/models/__init__.py index 804e5c2..8758fa1 100644 --- a/src/youdotcom/models/__init__.py +++ b/src/youdotcom/models/__init__.py @@ -16,9 +16,9 @@ from .agentruns422response_error import Detail, DetailTypedDict, Loc, LocTypedDict from .agentrunsbatchresponse import ( AgentRunsBatchResponse, + AgentRunsBatchResponseInput, + AgentRunsBatchResponseInputTypedDict, AgentRunsBatchResponseTypedDict, - Input1, - Input1TypedDict, Role, ) from .agentrunsresponseoutput import ( @@ -37,6 +37,7 @@ DataTypedDict, ) from .agentsrunsop import ( + AGENTS_RUNS_OP_SERVERS, AgentsRunsRequest, AgentsRunsRequestTypedDict, AgentsRunsResponse, @@ -62,31 +63,76 @@ ExpressAgentRunsRequest, ExpressAgentRunsRequestTypedDict, ) + from .finance_researchop import ( + FinanceResearchContentType, + FinanceResearchDetail, + FinanceResearchDetailTypedDict, + FinanceResearchInput, + FinanceResearchInputTypedDict, + FinanceResearchInputUnion, + FinanceResearchInputUnionTypedDict, + FinanceResearchLoc, + FinanceResearchLocTypedDict, + FinanceResearchOutput, + FinanceResearchOutputTypedDict, + FinanceResearchRequest, + FinanceResearchRequestTypedDict, + FinanceResearchResponse, + FinanceResearchResponseTypedDict, + FinanceResearchSource, + FinanceResearchSourceTypedDict, + ) + from .financeresearcheffort import FinanceResearchEffort from .freshness import Freshness + from .freshnessvalue import FreshnessValue, FreshnessValueTypedDict + from .getresearchtaskop import ( + GetResearchTaskRequest, + GetResearchTaskRequestTypedDict, + ) from .language import Language from .livecrawl import LiveCrawl from .livecrawlformats import LiveCrawlFormats + from .newsresult import NewsResult, NewsResultTypedDict from .reportverbosity import ReportVerbosity + from .researcheffort import ResearchEffort from .researchop import ( - ContentType, - Input2, - Input2TypedDict, - Output, - OutputTypedDict, + OutputSchema, + OutputSchemaTypedDict, ResearchDetail, ResearchDetailTypedDict, - ResearchEffort, ResearchInput, ResearchInputTypedDict, + ResearchInputUnion, + ResearchInputUnionTypedDict, ResearchLoc, ResearchLocTypedDict, ResearchRequest, ResearchRequestTypedDict, + ResearchResponse1, + ResearchResponse1TypedDict, + SourceControl, + SourceControlTypedDict, + ) + from .researchresponse import ( + Content, + ContentType, + ContentTypedDict, + ContentUnion, + ContentUnionTypedDict, + Output, + OutputTypedDict, ResearchResponse, ResearchResponseTypedDict, Source, SourceTypedDict, ) + from .researchtaskstreamevent import ( + Event, + ResearchTaskStreamEvent, + ResearchTaskStreamEventData, + ResearchTaskStreamEventDataTypedDict, + ResearchTaskStreamEventTypedDict, + ) from .researchtool import ResearchTool, ResearchToolTypedDict from .response_created import ResponseCreated, ResponseCreatedTypedDict from .response_done import ( @@ -122,39 +168,42 @@ from .response_starting import ResponseStarting, ResponseStartingTypedDict from .safesearch import SafeSearch from .searcheffort import SearchEffort - from .searchop import ( - Metadata, - MetadataTypedDict, - News, - NewsTypedDict, + from .searchmetadata import SearchMetadata, SearchMetadataTypedDict + from .searchop import SEARCH_OP_SERVERS, SearchRequest, SearchRequestTypedDict + from .searchpostop import SEARCH_POST_OP_SERVERS + from .searchrequestbody import SearchRequestBody, SearchRequestBodyTypedDict + from .searchresponse import ( Results, ResultsTypedDict, - SEARCH_OP_SERVERS, - SearchCountry, - SearchCountryTypedDict, - SearchFreshness, - SearchFreshnessTypedDict, - SearchLivecrawl, - SearchLivecrawlFormats, - SearchLivecrawlFormatsTypedDict, - SearchLivecrawlTypedDict, - SearchRequest, - SearchRequestTypedDict, SearchResponse, SearchResponseTypedDict, - SearchSafesearch, - SearchSafesearchTypedDict, - Web, - WebTypedDict, ) from .security import Security, SecurityTypedDict + from .streamresearchtaskop import ( + StreamResearchTaskRequest, + StreamResearchTaskRequestTypedDict, + ) + from .taskdetail import ( + Result, + ResultTypedDict, + TaskDetail, + TaskDetailInput, + TaskDetailInputTypedDict, + TaskDetailStatus, + TaskDetailTypedDict, + ) + from .taskresponse import TaskResponse, TaskResponseStatus, TaskResponseTypedDict from .verbosity import Verbosity + from .webresult import WebResult, WebResultTypedDict from .websearchtool import WebSearchTool, WebSearchToolTypedDict __all__ = [ + "AGENTS_RUNS_OP_SERVERS", "AdvancedAgentRunsRequest", "AdvancedAgentRunsRequestTypedDict", "AgentRunsBatchResponse", + "AgentRunsBatchResponseInput", + "AgentRunsBatchResponseInputTypedDict", "AgentRunsBatchResponseTypedDict", "AgentRunsResponseOutput", "AgentRunsResponseOutputTypedDict", @@ -169,7 +218,11 @@ "CONTENTS_OP_SERVERS", "ComputeTool", "ComputeToolTypedDict", + "Content", "ContentType", + "ContentTypedDict", + "ContentUnion", + "ContentUnionTypedDict", "Contents", "ContentsFormats", "ContentsMetadata", @@ -186,23 +239,42 @@ "DataTypedDict", "Detail", "DetailTypedDict", + "Event", "ExpressAgentRunsRequest", "ExpressAgentRunsRequestTypedDict", + "FinanceResearchContentType", + "FinanceResearchDetail", + "FinanceResearchDetailTypedDict", + "FinanceResearchEffort", + "FinanceResearchInput", + "FinanceResearchInputTypedDict", + "FinanceResearchInputUnion", + "FinanceResearchInputUnionTypedDict", + "FinanceResearchLoc", + "FinanceResearchLocTypedDict", + "FinanceResearchOutput", + "FinanceResearchOutputTypedDict", + "FinanceResearchRequest", + "FinanceResearchRequestTypedDict", + "FinanceResearchResponse", + "FinanceResearchResponseTypedDict", + "FinanceResearchSource", + "FinanceResearchSourceTypedDict", "Freshness", - "Input1", - "Input1TypedDict", - "Input2", - "Input2TypedDict", + "FreshnessValue", + "FreshnessValueTypedDict", + "GetResearchTaskRequest", + "GetResearchTaskRequestTypedDict", "Language", "LiveCrawl", "LiveCrawlFormats", "Loc", "LocTypedDict", - "Metadata", - "MetadataTypedDict", - "News", - "NewsTypedDict", + "NewsResult", + "NewsResultTypedDict", "Output", + "OutputSchema", + "OutputSchemaTypedDict", "OutputTypedDict", "ReportVerbosity", "ResearchDetail", @@ -210,12 +282,20 @@ "ResearchEffort", "ResearchInput", "ResearchInputTypedDict", + "ResearchInputUnion", + "ResearchInputUnionTypedDict", "ResearchLoc", "ResearchLocTypedDict", "ResearchRequest", "ResearchRequestTypedDict", "ResearchResponse", + "ResearchResponse1", + "ResearchResponse1TypedDict", "ResearchResponseTypedDict", + "ResearchTaskStreamEvent", + "ResearchTaskStreamEventData", + "ResearchTaskStreamEventDataTypedDict", + "ResearchTaskStreamEventTypedDict", "ResearchTool", "ResearchToolTypedDict", "ResponseCreated", @@ -242,38 +322,47 @@ "ResponseOutputTextDeltaTypedDict", "ResponseStarting", "ResponseStartingTypedDict", + "Result", + "ResultTypedDict", "Results", "ResultsTypedDict", "Role", "SEARCH_OP_SERVERS", + "SEARCH_POST_OP_SERVERS", "SafeSearch", - "SearchCountry", - "SearchCountryTypedDict", "SearchEffort", - "SearchFreshness", - "SearchFreshnessTypedDict", - "SearchLivecrawl", - "SearchLivecrawlFormats", - "SearchLivecrawlFormatsTypedDict", - "SearchLivecrawlTypedDict", + "SearchMetadata", + "SearchMetadataTypedDict", "SearchRequest", + "SearchRequestBody", + "SearchRequestBodyTypedDict", "SearchRequestTypedDict", "SearchResponse", "SearchResponseTypedDict", - "SearchSafesearch", - "SearchSafesearchTypedDict", "Security", "SecurityTypedDict", "Source", + "SourceControl", + "SourceControlTypedDict", "SourceTypedDict", + "StreamResearchTaskRequest", + "StreamResearchTaskRequestTypedDict", + "TaskDetail", + "TaskDetailInput", + "TaskDetailInputTypedDict", + "TaskDetailStatus", + "TaskDetailTypedDict", + "TaskResponse", + "TaskResponseStatus", + "TaskResponseTypedDict", "Tool", "ToolTypedDict", "Type", "Verbosity", - "Web", + "WebResult", + "WebResultTypedDict", "WebSearchTool", "WebSearchToolTypedDict", - "WebTypedDict", "WorkflowConfig", "WorkflowConfigTypedDict", ] @@ -290,9 +379,9 @@ "Loc": ".agentruns422response_error", "LocTypedDict": ".agentruns422response_error", "AgentRunsBatchResponse": ".agentrunsbatchresponse", + "AgentRunsBatchResponseInput": ".agentrunsbatchresponse", + "AgentRunsBatchResponseInputTypedDict": ".agentrunsbatchresponse", "AgentRunsBatchResponseTypedDict": ".agentrunsbatchresponse", - "Input1": ".agentrunsbatchresponse", - "Input1TypedDict": ".agentrunsbatchresponse", "Role": ".agentrunsbatchresponse", "AgentRunsResponseOutput": ".agentrunsresponseoutput", "AgentRunsResponseOutputTypedDict": ".agentrunsresponseoutput", @@ -303,6 +392,7 @@ "AgentRunsStreamingResponseTypedDict": ".agentrunsstreamingresponse", "Data": ".agentrunsstreamingresponse", "DataTypedDict": ".agentrunsstreamingresponse", + "AGENTS_RUNS_OP_SERVERS": ".agentsrunsop", "AgentsRunsRequest": ".agentsrunsop", "AgentsRunsRequestTypedDict": ".agentsrunsop", "AgentsRunsResponse": ".agentsrunsop", @@ -324,29 +414,68 @@ "CustomAgentRunsRequestTypedDict": ".customagentrunsrequest", "ExpressAgentRunsRequest": ".expressagentrunsrequest", "ExpressAgentRunsRequestTypedDict": ".expressagentrunsrequest", + "FinanceResearchContentType": ".finance_researchop", + "FinanceResearchDetail": ".finance_researchop", + "FinanceResearchDetailTypedDict": ".finance_researchop", + "FinanceResearchInput": ".finance_researchop", + "FinanceResearchInputTypedDict": ".finance_researchop", + "FinanceResearchInputUnion": ".finance_researchop", + "FinanceResearchInputUnionTypedDict": ".finance_researchop", + "FinanceResearchLoc": ".finance_researchop", + "FinanceResearchLocTypedDict": ".finance_researchop", + "FinanceResearchOutput": ".finance_researchop", + "FinanceResearchOutputTypedDict": ".finance_researchop", + "FinanceResearchRequest": ".finance_researchop", + "FinanceResearchRequestTypedDict": ".finance_researchop", + "FinanceResearchResponse": ".finance_researchop", + "FinanceResearchResponseTypedDict": ".finance_researchop", + "FinanceResearchSource": ".finance_researchop", + "FinanceResearchSourceTypedDict": ".finance_researchop", + "FinanceResearchEffort": ".financeresearcheffort", "Freshness": ".freshness", + "FreshnessValue": ".freshnessvalue", + "FreshnessValueTypedDict": ".freshnessvalue", + "GetResearchTaskRequest": ".getresearchtaskop", + "GetResearchTaskRequestTypedDict": ".getresearchtaskop", "Language": ".language", "LiveCrawl": ".livecrawl", "LiveCrawlFormats": ".livecrawlformats", + "NewsResult": ".newsresult", + "NewsResultTypedDict": ".newsresult", "ReportVerbosity": ".reportverbosity", - "ContentType": ".researchop", - "Input2": ".researchop", - "Input2TypedDict": ".researchop", - "Output": ".researchop", - "OutputTypedDict": ".researchop", + "ResearchEffort": ".researcheffort", + "OutputSchema": ".researchop", + "OutputSchemaTypedDict": ".researchop", "ResearchDetail": ".researchop", "ResearchDetailTypedDict": ".researchop", - "ResearchEffort": ".researchop", "ResearchInput": ".researchop", "ResearchInputTypedDict": ".researchop", + "ResearchInputUnion": ".researchop", + "ResearchInputUnionTypedDict": ".researchop", "ResearchLoc": ".researchop", "ResearchLocTypedDict": ".researchop", "ResearchRequest": ".researchop", "ResearchRequestTypedDict": ".researchop", - "ResearchResponse": ".researchop", - "ResearchResponseTypedDict": ".researchop", - "Source": ".researchop", - "SourceTypedDict": ".researchop", + "ResearchResponse1": ".researchop", + "ResearchResponse1TypedDict": ".researchop", + "SourceControl": ".researchop", + "SourceControlTypedDict": ".researchop", + "Content": ".researchresponse", + "ContentType": ".researchresponse", + "ContentTypedDict": ".researchresponse", + "ContentUnion": ".researchresponse", + "ContentUnionTypedDict": ".researchresponse", + "Output": ".researchresponse", + "OutputTypedDict": ".researchresponse", + "ResearchResponse": ".researchresponse", + "ResearchResponseTypedDict": ".researchresponse", + "Source": ".researchresponse", + "SourceTypedDict": ".researchresponse", + "Event": ".researchtaskstreamevent", + "ResearchTaskStreamEvent": ".researchtaskstreamevent", + "ResearchTaskStreamEventData": ".researchtaskstreamevent", + "ResearchTaskStreamEventDataTypedDict": ".researchtaskstreamevent", + "ResearchTaskStreamEventTypedDict": ".researchtaskstreamevent", "ResearchTool": ".researchtool", "ResearchToolTypedDict": ".researchtool", "ResponseCreated": ".response_created", @@ -375,32 +504,35 @@ "ResponseStartingTypedDict": ".response_starting", "SafeSearch": ".safesearch", "SearchEffort": ".searcheffort", - "Metadata": ".searchop", - "MetadataTypedDict": ".searchop", - "News": ".searchop", - "NewsTypedDict": ".searchop", - "Results": ".searchop", - "ResultsTypedDict": ".searchop", + "SearchMetadata": ".searchmetadata", + "SearchMetadataTypedDict": ".searchmetadata", "SEARCH_OP_SERVERS": ".searchop", - "SearchCountry": ".searchop", - "SearchCountryTypedDict": ".searchop", - "SearchFreshness": ".searchop", - "SearchFreshnessTypedDict": ".searchop", - "SearchLivecrawl": ".searchop", - "SearchLivecrawlFormats": ".searchop", - "SearchLivecrawlFormatsTypedDict": ".searchop", - "SearchLivecrawlTypedDict": ".searchop", "SearchRequest": ".searchop", "SearchRequestTypedDict": ".searchop", - "SearchResponse": ".searchop", - "SearchResponseTypedDict": ".searchop", - "SearchSafesearch": ".searchop", - "SearchSafesearchTypedDict": ".searchop", - "Web": ".searchop", - "WebTypedDict": ".searchop", + "SEARCH_POST_OP_SERVERS": ".searchpostop", + "SearchRequestBody": ".searchrequestbody", + "SearchRequestBodyTypedDict": ".searchrequestbody", + "Results": ".searchresponse", + "ResultsTypedDict": ".searchresponse", + "SearchResponse": ".searchresponse", + "SearchResponseTypedDict": ".searchresponse", "Security": ".security", "SecurityTypedDict": ".security", + "StreamResearchTaskRequest": ".streamresearchtaskop", + "StreamResearchTaskRequestTypedDict": ".streamresearchtaskop", + "Result": ".taskdetail", + "ResultTypedDict": ".taskdetail", + "TaskDetail": ".taskdetail", + "TaskDetailInput": ".taskdetail", + "TaskDetailInputTypedDict": ".taskdetail", + "TaskDetailStatus": ".taskdetail", + "TaskDetailTypedDict": ".taskdetail", + "TaskResponse": ".taskresponse", + "TaskResponseStatus": ".taskresponse", + "TaskResponseTypedDict": ".taskresponse", "Verbosity": ".verbosity", + "WebResult": ".webresult", + "WebResultTypedDict": ".webresult", "WebSearchTool": ".websearchtool", "WebSearchToolTypedDict": ".websearchtool", } diff --git a/src/youdotcom/models/advancedagentrunsrequest.py b/src/youdotcom/models/advancedagentrunsrequest.py index f271b2b..8511c0a 100644 --- a/src/youdotcom/models/advancedagentrunsrequest.py +++ b/src/youdotcom/models/advancedagentrunsrequest.py @@ -40,7 +40,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: @@ -94,7 +94,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/agentrunsbatchresponse.py b/src/youdotcom/models/agentrunsbatchresponse.py index 187616c..6de8970 100644 --- a/src/youdotcom/models/agentrunsbatchresponse.py +++ b/src/youdotcom/models/agentrunsbatchresponse.py @@ -18,14 +18,14 @@ class Role(str, Enum): USER = "user" -class Input1TypedDict(TypedDict): +class AgentRunsBatchResponseInputTypedDict(TypedDict): role: Role r"""The access based role of the user""" content: str r"""The question populated in the request payload""" -class Input1(BaseModel): +class AgentRunsBatchResponseInput(BaseModel): role: Role r"""The access based role of the user""" @@ -36,7 +36,7 @@ class Input1(BaseModel): class AgentRunsBatchResponseTypedDict(TypedDict): agent: str r"""The id of the agent populated in the request.""" - input: List[Input1TypedDict] + input: List[AgentRunsBatchResponseInputTypedDict] r"""The users access role and question you asked the agent""" output: List[AgentRunsResponseOutputTypedDict] r"""Array of response outputs from the agent""" @@ -48,7 +48,7 @@ class AgentRunsBatchResponse(BaseModel): agent: str r"""The id of the agent populated in the request.""" - input: List[Input1] + input: List[AgentRunsBatchResponseInput] r"""The users access role and question you asked the agent""" output: List[AgentRunsResponseOutput] @@ -65,7 +65,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/agentrunsresponseoutput.py b/src/youdotcom/models/agentrunsresponseoutput.py index 14726f5..1eaa631 100644 --- a/src/youdotcom/models/agentrunsresponseoutput.py +++ b/src/youdotcom/models/agentrunsresponseoutput.py @@ -69,7 +69,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/agentrunsresponsewebsearchresult.py b/src/youdotcom/models/agentrunsresponsewebsearchresult.py index a0a105c..7f9d3c3 100644 --- a/src/youdotcom/models/agentrunsresponsewebsearchresult.py +++ b/src/youdotcom/models/agentrunsresponsewebsearchresult.py @@ -64,7 +64,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/agentsrunsop.py b/src/youdotcom/models/agentsrunsop.py index 2ac90c0..52a57d5 100644 --- a/src/youdotcom/models/agentsrunsop.py +++ b/src/youdotcom/models/agentsrunsop.py @@ -26,6 +26,11 @@ from youdotcom.utils import eventstreaming +AGENTS_RUNS_OP_SERVERS = [ + "https://api.you.com", +] + + AgentsRunsRequestTypedDict = TypeAliasType( "AgentsRunsRequestTypedDict", Union[ diff --git a/src/youdotcom/models/contents.py b/src/youdotcom/models/contents.py index ecf165e..37d69b8 100644 --- a/src/youdotcom/models/contents.py +++ b/src/youdotcom/models/contents.py @@ -33,7 +33,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/contentsmetadata.py b/src/youdotcom/models/contentsmetadata.py index 6324e63..b6b8e50 100644 --- a/src/youdotcom/models/contentsmetadata.py +++ b/src/youdotcom/models/contentsmetadata.py @@ -34,7 +34,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) is_nullable_and_explicitly_set = ( k in nullable_fields and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member diff --git a/src/youdotcom/models/contentsop.py b/src/youdotcom/models/contentsop.py index fc47c7f..9e10292 100644 --- a/src/youdotcom/models/contentsop.py +++ b/src/youdotcom/models/contentsop.py @@ -21,6 +21,8 @@ class ContentsRequestTypedDict(TypedDict): r"""Array of content formats to return. All included formats are returned in the response. Include \"metadata\" to get JSON-LD and OpenGraph information, if available.""" crawl_timeout: NotRequired[int] r"""Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds.""" + max_age: NotRequired[Nullable[int]] + r"""Maximum allowed age of cached content in seconds. When set, cached content older than this threshold is ignored and the page is re-fetched. Must be 0 or greater. Default: null (no age limit, cached content is returned regardless of age).""" class ContentsRequest(BaseModel): @@ -33,18 +35,34 @@ class ContentsRequest(BaseModel): crawl_timeout: Optional[int] = 10 r"""Maximum time in seconds to wait for page content. Must be between 1 and 60 seconds. Default is 10 seconds.""" + max_age: OptionalNullable[int] = None + r"""Maximum allowed age of cached content in seconds. When set, cached content older than this threshold is ignored and the page is re-fetched. Must be 0 or greater. Default: null (no age limit, cached content is returned regardless of age).""" + @model_serializer(mode="wrap") def serialize_model(self, handler): - optional_fields = set(["urls", "formats", "crawl_timeout"]) + optional_fields = set(["urls", "formats", "crawl_timeout", "max_age"]) + nullable_fields = set(["max_age"]) + null_default_fields = set(["max_age"]) serialized = handler(self) m = {} for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) + is_nullable_and_explicitly_set = ( + k in nullable_fields + and ( + self.__pydantic_fields_set__.intersection({n}) + or k in null_default_fields + ) # pylint: disable=no-member + ) if val != UNSET_SENTINEL: - if val is not None or k not in optional_fields: + if ( + val is not None + or k not in optional_fields + or is_nullable_and_explicitly_set + ): m[k] = val return m @@ -88,7 +106,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) is_nullable_and_explicitly_set = ( k in nullable_fields and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member diff --git a/src/youdotcom/models/customagentrunsrequest.py b/src/youdotcom/models/customagentrunsrequest.py index 759f45b..181e560 100644 --- a/src/youdotcom/models/customagentrunsrequest.py +++ b/src/youdotcom/models/customagentrunsrequest.py @@ -34,7 +34,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/expressagentrunsrequest.py b/src/youdotcom/models/expressagentrunsrequest.py index f4f5de0..8211741 100644 --- a/src/youdotcom/models/expressagentrunsrequest.py +++ b/src/youdotcom/models/expressagentrunsrequest.py @@ -46,7 +46,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/finance_researchop.py b/src/youdotcom/models/finance_researchop.py new file mode 100644 index 0000000..1d886fd --- /dev/null +++ b/src/youdotcom/models/finance_researchop.py @@ -0,0 +1,205 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .financeresearcheffort import FinanceResearchEffort +from enum import Enum +from pydantic import model_serializer +from typing import Any, Dict, List, Optional, Union +from typing_extensions import NotRequired, TypeAliasType, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class FinanceResearchRequestTypedDict(TypedDict): + input: str + r"""The financial research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + """ + research_effort: NotRequired[FinanceResearchEffort] + r"""Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. + """ + + +class FinanceResearchRequest(BaseModel): + input: str + r"""The financial research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + """ + + research_effort: Optional[FinanceResearchEffort] = FinanceResearchEffort.DEEP + r"""Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. + """ + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["research_effort"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +FinanceResearchLocTypedDict = TypeAliasType( + "FinanceResearchLocTypedDict", Union[str, int] +) + + +FinanceResearchLoc = TypeAliasType("FinanceResearchLoc", Union[str, int]) + + +class FinanceResearchInputTypedDict(TypedDict): + pass + + +class FinanceResearchInput(BaseModel): + pass + + +FinanceResearchInputUnionTypedDict = TypeAliasType( + "FinanceResearchInputUnionTypedDict", Union[FinanceResearchInputTypedDict, str] +) +r"""The input value that caused the error.""" + + +FinanceResearchInputUnion = TypeAliasType( + "FinanceResearchInputUnion", Union[FinanceResearchInput, str] +) +r"""The input value that caused the error.""" + + +class FinanceResearchDetailTypedDict(TypedDict): + type: str + r"""The validation error type.""" + loc: List[FinanceResearchLocTypedDict] + r"""The location of the error as a path of segments (strings for field names, integers for byte offsets).""" + msg: str + r"""A human-readable description of the error.""" + input: FinanceResearchInputUnionTypedDict + r"""The input value that caused the error.""" + ctx: NotRequired[Dict[str, Any]] + r"""Additional context about the error.""" + + +class FinanceResearchDetail(BaseModel): + type: str + r"""The validation error type.""" + + loc: List[FinanceResearchLoc] + r"""The location of the error as a path of segments (strings for field names, integers for byte offsets).""" + + msg: str + r"""A human-readable description of the error.""" + + input: FinanceResearchInputUnion + r"""The input value that caused the error.""" + + ctx: Optional[Dict[str, Any]] = None + r"""Additional context about the error.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["ctx"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +class FinanceResearchContentType(str, Enum): + r"""The format of the content field.""" + + TEXT = "text" + + +class FinanceResearchSourceTypedDict(TypedDict): + url: str + r"""The URL of the source webpage.""" + title: NotRequired[str] + r"""The title of the source webpage.""" + + +class FinanceResearchSource(BaseModel): + url: str + r"""The URL of the source webpage.""" + + title: Optional[str] = None + r"""The title of the source webpage.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["title"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +class FinanceResearchOutputTypedDict(TypedDict): + r"""The research output containing the answer and sources.""" + + content: str + r"""The comprehensive finance-grade response with inline citations. Content is a Markdown string with numbered citations that reference the items in the sources array.""" + content_type: FinanceResearchContentType + r"""The format of the content field.""" + sources: List[FinanceResearchSourceTypedDict] + r"""A list of web sources used to generate the answer.""" + + +class FinanceResearchOutput(BaseModel): + r"""The research output containing the answer and sources.""" + + content: str + r"""The comprehensive finance-grade response with inline citations. Content is a Markdown string with numbered citations that reference the items in the sources array.""" + + content_type: FinanceResearchContentType + r"""The format of the content field.""" + + sources: List[FinanceResearchSource] + r"""A list of web sources used to generate the answer.""" + + +class FinanceResearchResponseTypedDict(TypedDict): + r"""A JSON object containing a comprehensive finance-grade answer with citations and supporting search results""" + + output: FinanceResearchOutputTypedDict + r"""The research output containing the answer and sources.""" + + +class FinanceResearchResponse(BaseModel): + r"""A JSON object containing a comprehensive finance-grade answer with citations and supporting search results""" + + output: FinanceResearchOutput + r"""The research output containing the answer and sources.""" diff --git a/src/youdotcom/models/financeresearcheffort.py b/src/youdotcom/models/financeresearcheffort.py new file mode 100644 index 0000000..417d805 --- /dev/null +++ b/src/youdotcom/models/financeresearcheffort.py @@ -0,0 +1,16 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from enum import Enum + + +class FinanceResearchEffort(str, Enum): + r"""Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. + """ + + DEEP = "deep" + EXHAUSTIVE = "exhaustive" diff --git a/src/youdotcom/models/freshness.py b/src/youdotcom/models/freshness.py index da5bd9f..83281eb 100644 --- a/src/youdotcom/models/freshness.py +++ b/src/youdotcom/models/freshness.py @@ -5,8 +5,6 @@ class Freshness(str, Enum): - r"""Specifies the freshness of the results to return.""" - DAY = "day" WEEK = "week" MONTH = "month" diff --git a/src/youdotcom/models/freshnessvalue.py b/src/youdotcom/models/freshnessvalue.py new file mode 100644 index 0000000..66ff778 --- /dev/null +++ b/src/youdotcom/models/freshnessvalue.py @@ -0,0 +1,22 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .freshness import Freshness +from typing import Union +from typing_extensions import TypeAliasType + + +FreshnessValueTypedDict = TypeAliasType( + "FreshnessValueTypedDict", Union[Freshness, str] +) +r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + +When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. +""" + + +FreshnessValue = TypeAliasType("FreshnessValue", Union[Freshness, str]) +r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + +When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. +""" diff --git a/src/youdotcom/models/getresearchtaskop.py b/src/youdotcom/models/getresearchtaskop.py new file mode 100644 index 0000000..0330943 --- /dev/null +++ b/src/youdotcom/models/getresearchtaskop.py @@ -0,0 +1,18 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from typing_extensions import Annotated, TypedDict +from youdotcom.types import BaseModel +from youdotcom.utils import FieldMetadata, PathParamMetadata + + +class GetResearchTaskRequestTypedDict(TypedDict): + task_id: str + r"""The UUID of the research task.""" + + +class GetResearchTaskRequest(BaseModel): + task_id: Annotated[ + str, FieldMetadata(path=PathParamMetadata(style="simple", explode=False)) + ] + r"""The UUID of the research task.""" diff --git a/src/youdotcom/models/language.py b/src/youdotcom/models/language.py index 25de233..83704f1 100644 --- a/src/youdotcom/models/language.py +++ b/src/youdotcom/models/language.py @@ -5,6 +5,8 @@ class Language(str, Enum): + r"""The language of the web results that will be returned (BCP 47 format).""" + AR = "AR" EU = "EU" BN = "BN" @@ -30,7 +32,7 @@ class Language(str, Enum): HU = "HU" IS = "IS" IT = "IT" - JP = "JP" + JA = "JA" KN = "KN" KO = "KO" LV = "LV" diff --git a/src/youdotcom/models/livecrawlformats.py b/src/youdotcom/models/livecrawlformats.py index 27c9da8..ceca02d 100644 --- a/src/youdotcom/models/livecrawlformats.py +++ b/src/youdotcom/models/livecrawlformats.py @@ -5,7 +5,5 @@ class LiveCrawlFormats(str, Enum): - r"""Indicates the format of the livecrawled content.""" - HTML = "html" MARKDOWN = "markdown" diff --git a/src/youdotcom/models/newsresult.py b/src/youdotcom/models/newsresult.py new file mode 100644 index 0000000..9a89295 --- /dev/null +++ b/src/youdotcom/models/newsresult.py @@ -0,0 +1,62 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .contents import Contents, ContentsTypedDict +from datetime import datetime +from pydantic import model_serializer +from typing import Optional +from typing_extensions import NotRequired, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class NewsResultTypedDict(TypedDict): + title: NotRequired[str] + r"""The title of the news result.""" + description: NotRequired[str] + r"""A brief description of the content of the news result.""" + page_age: NotRequired[datetime] + r"""UTC timestamp of the article's publication date.""" + thumbnail_url: NotRequired[str] + r"""URL of the thumbnail.""" + url: NotRequired[str] + r"""The URL of the news result.""" + contents: NotRequired[ContentsTypedDict] + r"""Contents of the page if livecrawl was enabled.""" + + +class NewsResult(BaseModel): + title: Optional[str] = None + r"""The title of the news result.""" + + description: Optional[str] = None + r"""A brief description of the content of the news result.""" + + page_age: Optional[datetime] = None + r"""UTC timestamp of the article's publication date.""" + + thumbnail_url: Optional[str] = None + r"""URL of the thumbnail.""" + + url: Optional[str] = None + r"""The URL of the news result.""" + + contents: Optional[Contents] = None + r"""Contents of the page if livecrawl was enabled.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + ["title", "description", "page_age", "thumbnail_url", "url", "contents"] + ) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/src/youdotcom/models/researcheffort.py b/src/youdotcom/models/researcheffort.py new file mode 100644 index 0000000..2384195 --- /dev/null +++ b/src/youdotcom/models/researcheffort.py @@ -0,0 +1,20 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from enum import Enum + + +class ResearchEffort(str, Enum): + r"""Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. + - `standard`: The default. Balances speed and depth, a good fit for most questions. + - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + """ + + LITE = "lite" + STANDARD = "standard" + DEEP = "deep" + EXHAUSTIVE = "exhaustive" diff --git a/src/youdotcom/models/researchop.py b/src/youdotcom/models/researchop.py index f8ec45c..d4fbf57 100644 --- a/src/youdotcom/models/researchop.py +++ b/src/youdotcom/models/researchop.py @@ -1,27 +1,95 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" from __future__ import annotations -from enum import Enum +from .researcheffort import ResearchEffort +from .researchresponse import ResearchResponse, ResearchResponseTypedDict +from .taskresponse import TaskResponse, TaskResponseTypedDict from pydantic import model_serializer from typing import Any, Dict, List, Optional, Union from typing_extensions import NotRequired, TypeAliasType, TypedDict from youdotcom.types import BaseModel, UNSET_SENTINEL -class ResearchEffort(str, Enum): - r"""Controls how much time and effort the Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. +class SourceControlTypedDict(TypedDict): + r"""Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country. - Available levels: - - `lite`: Returns answers quickly. Good for straightforward questions that just need a fast, reliable answer. - - `standard`: The default. Balances speed and depth, a good fit for most questions. - - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. - - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + `include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. + """ + + include_domains: NotRequired[List[str]] + r"""Only return results from these domains. Max 500 domains. Cannot be used with exclude_domains or boost_domains.""" + exclude_domains: NotRequired[List[str]] + r"""Never return results from these domains. Max 500 domains. Also blocks the research agent from visiting pages on those domains during browsing.""" + boost_domains: NotRequired[List[str]] + r"""Boost results from these domains without excluding other domains. Max 500 domains. Cannot be used with include_domains.""" + freshness: NotRequired[str] + r"""Filter results by recency. Accepts `day`, `week`, `month`, `year`, or a custom date range in `YYYY-MM-DDtoYYYY-MM-DD` format.""" + country: NotRequired[str] + r"""ISO 3166-1 alpha-2 country code, such as US, GB, or DE, to geographically focus web results.""" + + +class SourceControl(BaseModel): + r"""Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country. + + `include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. """ - LITE = "lite" - STANDARD = "standard" - DEEP = "deep" - EXHAUSTIVE = "exhaustive" + include_domains: Optional[List[str]] = None + r"""Only return results from these domains. Max 500 domains. Cannot be used with exclude_domains or boost_domains.""" + + exclude_domains: Optional[List[str]] = None + r"""Never return results from these domains. Max 500 domains. Also blocks the research agent from visiting pages on those domains during browsing.""" + + boost_domains: Optional[List[str]] = None + r"""Boost results from these domains without excluding other domains. Max 500 domains. Cannot be used with include_domains.""" + + freshness: Optional[str] = None + r"""Filter results by recency. Accepts `day`, `week`, `month`, `year`, or a custom date range in `YYYY-MM-DDtoYYYY-MM-DD` format.""" + + country: Optional[str] = None + r"""ISO 3166-1 alpha-2 country code, such as US, GB, or DE, to geographically focus web results.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + [ + "include_domains", + "exclude_domains", + "boost_domains", + "freshness", + "country", + ] + ) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +class OutputSchemaTypedDict(TypedDict): + r"""Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: \"lite\" returns 422. + + Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported. + + Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. + """ + + +class OutputSchema(BaseModel): + r"""Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: \"lite\" returns 422. + + Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported. + + Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. + """ class ResearchRequestTypedDict(TypedDict): @@ -39,6 +107,20 @@ class ResearchRequestTypedDict(TypedDict): - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. """ + background: NotRequired[bool] + r"""When true, queue a research task and return a task handle immediately instead of waiting for the result inline. Defaults to synchronous. When enabled, the response is a TaskResponse object with a task_id and stream_url for polling progress via GET /v1/research/{task_id} or streaming via GET /v1/research/{task_id}/stream.""" + source_control: NotRequired[SourceControlTypedDict] + r"""Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country. + + `include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. + """ + output_schema: NotRequired[OutputSchemaTypedDict] + r"""Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: \"lite\" returns 422. + + Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported. + + Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. + """ class ResearchRequest(BaseModel): @@ -58,15 +140,34 @@ class ResearchRequest(BaseModel): - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. """ + background: Optional[bool] = False + r"""When true, queue a research task and return a task handle immediately instead of waiting for the result inline. Defaults to synchronous. When enabled, the response is a TaskResponse object with a task_id and stream_url for polling progress via GET /v1/research/{task_id} or streaming via GET /v1/research/{task_id}/stream.""" + + source_control: Optional[SourceControl] = None + r"""Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country. + + `include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. + """ + + output_schema: Optional[OutputSchema] = None + r"""Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: \"lite\" returns 422. + + Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported. + + Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. + """ + @model_serializer(mode="wrap") def serialize_model(self, handler): - optional_fields = set(["research_effort"]) + optional_fields = set( + ["research_effort", "background", "source_control", "output_schema"] + ) serialized = handler(self) m = {} for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: @@ -89,11 +190,13 @@ class ResearchInput(BaseModel): pass -Input2TypedDict = TypeAliasType("Input2TypedDict", Union[ResearchInputTypedDict, str]) +ResearchInputUnionTypedDict = TypeAliasType( + "ResearchInputUnionTypedDict", Union[ResearchInputTypedDict, str] +) r"""The input value that caused the error.""" -Input2 = TypeAliasType("Input2", Union[ResearchInput, str]) +ResearchInputUnion = TypeAliasType("ResearchInputUnion", Union[ResearchInput, str]) r"""The input value that caused the error.""" @@ -104,7 +207,7 @@ class ResearchDetailTypedDict(TypedDict): r"""The location of the error as a path of segments (strings for field names, integers for byte offsets).""" msg: str r"""A human-readable description of the error.""" - input: Input2TypedDict + input: ResearchInputUnionTypedDict r"""The input value that caused the error.""" ctx: NotRequired[Dict[str, Any]] r"""Additional context about the error.""" @@ -120,7 +223,7 @@ class ResearchDetail(BaseModel): msg: str r"""A human-readable description of the error.""" - input: Input2 + input: ResearchInputUnion r"""The input value that caused the error.""" ctx: Optional[Dict[str, Any]] = None @@ -134,49 +237,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) - - if val != UNSET_SENTINEL: - if val is not None or k not in optional_fields: - m[k] = val - - return m - - -class ContentType(str, Enum): - r"""The format of the content field.""" - - TEXT = "text" - - -class SourceTypedDict(TypedDict): - url: str - r"""The URL of the source webpage.""" - title: NotRequired[str] - r"""The title of the source webpage.""" - snippets: NotRequired[List[str]] - r"""Relevant excerpts from the source page that were used in generating the answer.""" - - -class Source(BaseModel): - url: str - r"""The URL of the source webpage.""" - - title: Optional[str] = None - r"""The title of the source webpage.""" - - snippets: Optional[List[str]] = None - r"""Relevant excerpts from the source page that were used in generating the answer.""" - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["title", "snippets"]) - serialized = handler(self) - m = {} - - for n, f in type(self).model_fields.items(): - k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: @@ -185,39 +246,14 @@ def serialize_model(self, handler): return m -class OutputTypedDict(TypedDict): - r"""The research output containing the answer and sources.""" - - content: str - r"""The comprehensive response with inline citations. The content is formatted in Markdown and includes numbered citations that reference the items in the sources array.""" - content_type: ContentType - r"""The format of the content field.""" - sources: List[SourceTypedDict] - r"""A list of web sources used to generate the answer.""" - - -class Output(BaseModel): - r"""The research output containing the answer and sources.""" - - content: str - r"""The comprehensive response with inline citations. The content is formatted in Markdown and includes numbered citations that reference the items in the sources array.""" - - content_type: ContentType - r"""The format of the content field.""" - - sources: List[Source] - r"""A list of web sources used to generate the answer.""" - - -class ResearchResponseTypedDict(TypedDict): - r"""A JSON object containing a comprehensive answer with citations and supporting search results""" - - output: OutputTypedDict - r"""The research output containing the answer and sources.""" - +ResearchResponse1TypedDict = TypeAliasType( + "ResearchResponse1TypedDict", + Union[ResearchResponseTypedDict, TaskResponseTypedDict], +) +r"""A JSON object containing a comprehensive answer with citations and supporting search results. When background=true, returns a task handle instead.""" -class ResearchResponse(BaseModel): - r"""A JSON object containing a comprehensive answer with citations and supporting search results""" - output: Output - r"""The research output containing the answer and sources.""" +ResearchResponse1 = TypeAliasType( + "ResearchResponse1", Union[ResearchResponse, TaskResponse] +) +r"""A JSON object containing a comprehensive answer with citations and supporting search results. When background=true, returns a task handle instead.""" diff --git a/src/youdotcom/models/researchresponse.py b/src/youdotcom/models/researchresponse.py new file mode 100644 index 0000000..01ec387 --- /dev/null +++ b/src/youdotcom/models/researchresponse.py @@ -0,0 +1,103 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from enum import Enum +from pydantic import model_serializer +from typing import List, Optional, Union +from typing_extensions import NotRequired, TypeAliasType, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class ContentTypedDict(TypedDict): + pass + + +class Content(BaseModel): + pass + + +ContentUnionTypedDict = TypeAliasType( + "ContentUnionTypedDict", Union[ContentTypedDict, str] +) +r"""The comprehensive response with inline citations. When content_type is \"text\", this is a Markdown string with numbered citations that reference the items in the sources array. When content_type is \"object\", this is a structured JSON object matching the requested output_schema.""" + + +ContentUnion = TypeAliasType("ContentUnion", Union[Content, str]) +r"""The comprehensive response with inline citations. When content_type is \"text\", this is a Markdown string with numbered citations that reference the items in the sources array. When content_type is \"object\", this is a structured JSON object matching the requested output_schema.""" + + +class ContentType(str, Enum): + r"""The format of the content field.""" + + TEXT = "text" + OBJECT = "object" + + +class SourceTypedDict(TypedDict): + url: str + r"""The URL of the source webpage.""" + title: NotRequired[str] + r"""The title of the source webpage.""" + snippets: NotRequired[List[str]] + r"""Relevant excerpts from the source page that were used in generating the answer.""" + + +class Source(BaseModel): + url: str + r"""The URL of the source webpage.""" + + title: Optional[str] = None + r"""The title of the source webpage.""" + + snippets: Optional[List[str]] = None + r"""Relevant excerpts from the source page that were used in generating the answer.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["title", "snippets"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +class OutputTypedDict(TypedDict): + r"""The research output containing the answer and sources.""" + + content: ContentUnionTypedDict + r"""The comprehensive response with inline citations. When content_type is \"text\", this is a Markdown string with numbered citations that reference the items in the sources array. When content_type is \"object\", this is a structured JSON object matching the requested output_schema.""" + content_type: ContentType + r"""The format of the content field.""" + sources: List[SourceTypedDict] + r"""A list of web sources used to generate the answer.""" + + +class Output(BaseModel): + r"""The research output containing the answer and sources.""" + + content: ContentUnion + r"""The comprehensive response with inline citations. When content_type is \"text\", this is a Markdown string with numbered citations that reference the items in the sources array. When content_type is \"object\", this is a structured JSON object matching the requested output_schema.""" + + content_type: ContentType + r"""The format of the content field.""" + + sources: List[Source] + r"""A list of web sources used to generate the answer.""" + + +class ResearchResponseTypedDict(TypedDict): + output: OutputTypedDict + r"""The research output containing the answer and sources.""" + + +class ResearchResponse(BaseModel): + output: Output + r"""The research output containing the answer and sources.""" diff --git a/src/youdotcom/models/researchtaskstreamevent.py b/src/youdotcom/models/researchtaskstreamevent.py new file mode 100644 index 0000000..0198893 --- /dev/null +++ b/src/youdotcom/models/researchtaskstreamevent.py @@ -0,0 +1,116 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from enum import Enum +from pydantic import model_serializer +from typing import Any, Dict, Optional +from typing_extensions import NotRequired, TypedDict +from youdotcom.types import BaseModel, Nullable, OptionalNullable, UNSET, UNSET_SENTINEL + + +class Event(str, Enum): + r"""The type of the SSE event. Most streams start with a `connected` event and then deliver terminal events `response.done`, `complete`, `error`, or `cancelled` from the worker. + If the SSE stream has aged out (after ~15 minutes) without any events flowing and the task is already in a terminal state, the server emits a synthetic event whose name is the task's status: one of `completed`, `failed`, or `cancelled`. Treat these synthetic event names the same as the corresponding worker-emitted names (`complete` ↔ `completed`, `error` ↔ `failed`, `cancelled` == `cancelled`). + """ + + CONNECTED = "connected" + RESPONSE_DONE = "response.done" + COMPLETE = "complete" + COMPLETED = "completed" + ERROR = "error" + FAILED = "failed" + CANCELLED = "cancelled" + + +class ResearchTaskStreamEventDataTypedDict(TypedDict): + r"""The event payload. Structure varies by event type. Common fields include type, task_id, status, data (event-specific), error, and sequence.""" + + type: NotRequired[str] + r"""The event type identifier.""" + task_id: NotRequired[str] + r"""The task UUID.""" + status: NotRequired[str] + r"""Current task status when the event was emitted.""" + data: NotRequired[Dict[str, Any]] + r"""Event-specific payload data.""" + error: NotRequired[Nullable[str]] + r"""Error message if the event represents an error.""" + sequence: NotRequired[int] + r"""Event sequence number.""" + + +class ResearchTaskStreamEventData(BaseModel): + r"""The event payload. Structure varies by event type. Common fields include type, task_id, status, data (event-specific), error, and sequence.""" + + type: Optional[str] = None + r"""The event type identifier.""" + + task_id: Optional[str] = None + r"""The task UUID.""" + + status: Optional[str] = None + r"""Current task status when the event was emitted.""" + + data: Optional[Dict[str, Any]] = None + r"""Event-specific payload data.""" + + error: OptionalNullable[str] = UNSET + r"""Error message if the event represents an error.""" + + sequence: Optional[int] = None + r"""Event sequence number.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + ["type", "task_id", "status", "data", "error", "sequence"] + ) + nullable_fields = set(["error"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + is_nullable_and_explicitly_set = ( + k in nullable_fields + and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member + ) + + if val != UNSET_SENTINEL: + if ( + val is not None + or k not in optional_fields + or is_nullable_and_explicitly_set + ): + m[k] = val + + return m + + +class ResearchTaskStreamEventTypedDict(TypedDict): + r"""A server-sent event for a background research task stream.""" + + id: str + r"""Sequence number of the SSE event.""" + event: Event + r"""The type of the SSE event. Most streams start with a `connected` event and then deliver terminal events `response.done`, `complete`, `error`, or `cancelled` from the worker. + If the SSE stream has aged out (after ~15 minutes) without any events flowing and the task is already in a terminal state, the server emits a synthetic event whose name is the task's status: one of `completed`, `failed`, or `cancelled`. Treat these synthetic event names the same as the corresponding worker-emitted names (`complete` ↔ `completed`, `error` ↔ `failed`, `cancelled` == `cancelled`). + """ + data: ResearchTaskStreamEventDataTypedDict + r"""The event payload. Structure varies by event type. Common fields include type, task_id, status, data (event-specific), error, and sequence.""" + + +class ResearchTaskStreamEvent(BaseModel): + r"""A server-sent event for a background research task stream.""" + + id: str + r"""Sequence number of the SSE event.""" + + event: Event + r"""The type of the SSE event. Most streams start with a `connected` event and then deliver terminal events `response.done`, `complete`, `error`, or `cancelled` from the worker. + If the SSE stream has aged out (after ~15 minutes) without any events flowing and the task is already in a terminal state, the server emits a synthetic event whose name is the task's status: one of `completed`, `failed`, or `cancelled`. Treat these synthetic event names the same as the corresponding worker-emitted names (`complete` ↔ `completed`, `error` ↔ `failed`, `cancelled` == `cancelled`). + """ + + data: ResearchTaskStreamEventData + r"""The event payload. Structure varies by event type. Common fields include type, task_id, status, data (event-specific), error, and sequence.""" diff --git a/src/youdotcom/models/searchmetadata.py b/src/youdotcom/models/searchmetadata.py new file mode 100644 index 0000000..758b0c2 --- /dev/null +++ b/src/youdotcom/models/searchmetadata.py @@ -0,0 +1,39 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from pydantic import model_serializer +from typing import Optional +from typing_extensions import NotRequired, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class SearchMetadataTypedDict(TypedDict): + search_uuid: NotRequired[str] + query: NotRequired[str] + r"""Returns the search query used to retrieve the results.""" + latency: NotRequired[float] + + +class SearchMetadata(BaseModel): + search_uuid: Optional[str] = None + + query: Optional[str] = None + r"""Returns the search query used to retrieve the results.""" + + latency: Optional[float] = None + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["search_uuid", "query", "latency"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/src/youdotcom/models/searchop.py b/src/youdotcom/models/searchop.py index a06a0fd..358a4fa 100644 --- a/src/youdotcom/models/searchop.py +++ b/src/youdotcom/models/searchop.py @@ -1,17 +1,15 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" from __future__ import annotations -from .contents import Contents, ContentsTypedDict from .country import Country -from .freshness import Freshness +from .freshnessvalue import FreshnessValue, FreshnessValueTypedDict from .language import Language from .livecrawl import LiveCrawl from .livecrawlformats import LiveCrawlFormats from .safesearch import SafeSearch -from datetime import datetime from pydantic import model_serializer -from typing import List, Optional, Union -from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict +from typing import List, Optional +from typing_extensions import Annotated, NotRequired, TypedDict from youdotcom.types import BaseModel, UNSET_SENTINEL from youdotcom.utils import FieldMetadata, QueryParamMetadata @@ -21,100 +19,54 @@ ] -SearchFreshnessTypedDict = TypeAliasType( - "SearchFreshnessTypedDict", Union[Freshness, str] -) -r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. - -When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. -""" - - -SearchFreshness = TypeAliasType("SearchFreshness", Union[Freshness, str]) -r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. - -When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. -""" - - -SearchCountryTypedDict = TypeAliasType("SearchCountryTypedDict", Union[Country, str]) -r"""The country code that determines the geographical focus of the web results.""" - - -SearchCountry = TypeAliasType("SearchCountry", Union[Country, str]) -r"""The country code that determines the geographical focus of the web results.""" - - -SearchSafesearchTypedDict = TypeAliasType( - "SearchSafesearchTypedDict", Union[SafeSearch, str] -) -r"""Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not.""" - - -SearchSafesearch = TypeAliasType("SearchSafesearch", Union[SafeSearch, str]) -r"""Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not.""" - - -SearchLivecrawlTypedDict = TypeAliasType( - "SearchLivecrawlTypedDict", Union[LiveCrawl, str] -) -r"""Indicates which section(s) of search results to livecrawl and return full page content.""" - - -SearchLivecrawl = TypeAliasType("SearchLivecrawl", Union[LiveCrawl, str]) -r"""Indicates which section(s) of search results to livecrawl and return full page content.""" - - -SearchLivecrawlFormatsTypedDict = TypeAliasType( - "SearchLivecrawlFormatsTypedDict", Union[LiveCrawlFormats, str] -) -r"""Indicates the format of the livecrawled content.""" - - -SearchLivecrawlFormats = TypeAliasType( - "SearchLivecrawlFormats", Union[LiveCrawlFormats, str] -) -r"""Indicates the format of the livecrawled content.""" - - class SearchRequestTypedDict(TypedDict): query: str - r"""The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search.""" count: NotRequired[int] - r"""Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them).""" - freshness: NotRequired[SearchFreshnessTypedDict] + freshness: NotRequired[FreshnessValueTypedDict] r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. """ offset: NotRequired[int] - r"""Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`.""" - country: NotRequired[SearchCountryTypedDict] + country: NotRequired[Country] r"""The country code that determines the geographical focus of the web results.""" language: NotRequired[Language] r"""The language of the web results that will be returned (BCP 47 format).""" - safesearch: NotRequired[SearchSafesearchTypedDict] + safesearch: NotRequired[SafeSearch] r"""Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not.""" - livecrawl: NotRequired[SearchLivecrawlTypedDict] + livecrawl: NotRequired[LiveCrawl] r"""Indicates which section(s) of search results to livecrawl and return full page content.""" - livecrawl_formats: NotRequired[SearchLivecrawlFormatsTypedDict] - r"""Indicates the format of the livecrawled content.""" + livecrawl_formats: NotRequired[List[LiveCrawlFormats]] + include_domains: NotRequired[str] + r"""A list of domains to restrict search results to. Only results from these domains will be returned. For large domain lists (up to 500), use POST with a JSON array instead. This is a strict allowlist — cannot be combined with `exclude_domains` (returns `422`). + + **Important:** Use a single comma-separated value (e.g. `include_domains=nytimes.com,bbc.com`). Repeated parameters (`include_domains=a.com&include_domains=b.com`) are not supported. + """ + exclude_domains: NotRequired[str] + r"""A list of domains to exclude from search results. Results from these domains will be filtered out. For large domain lists (up to 500), use POST with a JSON array instead. Cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `exclude_domains=spam-site.com,other-site.com`). Repeated parameters are not supported. + """ + boost_domains: NotRequired[str] + r"""A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `boost_domains=nytimes.com,wired.com`). Repeated parameters are not supported. + """ + crawl_timeout: NotRequired[int] class SearchRequest(BaseModel): query: Annotated[ str, FieldMetadata(query=QueryParamMetadata(style="form", explode=True)) - ] = "Your query" - r"""The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search.""" + ] count: Annotated[ Optional[int], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), ] = 10 - r"""Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them).""" freshness: Annotated[ - Optional[SearchFreshness], + Optional[FreshnessValue], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), ] = None r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. @@ -126,10 +78,9 @@ class SearchRequest(BaseModel): Optional[int], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), ] = None - r"""Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`.""" country: Annotated[ - Optional[SearchCountry], + Optional[Country], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), ] = None r"""The country code that determines the geographical focus of the web results.""" @@ -141,22 +92,53 @@ class SearchRequest(BaseModel): r"""The language of the web results that will be returned (BCP 47 format).""" safesearch: Annotated[ - Optional[SearchSafesearch], + Optional[SafeSearch], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), ] = None r"""Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not.""" livecrawl: Annotated[ - Optional[SearchLivecrawl], + Optional[LiveCrawl], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), ] = None r"""Indicates which section(s) of search results to livecrawl and return full page content.""" livecrawl_formats: Annotated[ - Optional[SearchLivecrawlFormats], + Optional[List[LiveCrawlFormats]], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + + include_domains: Annotated[ + Optional[str], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""A list of domains to restrict search results to. Only results from these domains will be returned. For large domain lists (up to 500), use POST with a JSON array instead. This is a strict allowlist — cannot be combined with `exclude_domains` (returns `422`). + + **Important:** Use a single comma-separated value (e.g. `include_domains=nytimes.com,bbc.com`). Repeated parameters (`include_domains=a.com&include_domains=b.com`) are not supported. + """ + + exclude_domains: Annotated[ + Optional[str], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""A list of domains to exclude from search results. Results from these domains will be filtered out. For large domain lists (up to 500), use POST with a JSON array instead. Cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `exclude_domains=spam-site.com,other-site.com`). Repeated parameters are not supported. + """ + + boost_domains: Annotated[ + Optional[str], FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), ] = None - r"""Indicates the format of the livecrawled content.""" + r"""A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `boost_domains=nytimes.com,wired.com`). Repeated parameters are not supported. + """ + + crawl_timeout: Annotated[ + Optional[int], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = 10 @model_serializer(mode="wrap") def serialize_model(self, handler): @@ -170,6 +152,10 @@ def serialize_model(self, handler): "safesearch", "livecrawl", "livecrawl_formats", + "include_domains", + "exclude_domains", + "boost_domains", + "crawl_timeout", ] ) serialized = handler(self) @@ -177,228 +163,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) - - if val != UNSET_SENTINEL: - if val is not None or k not in optional_fields: - m[k] = val - - return m - - -class WebTypedDict(TypedDict): - url: NotRequired[str] - r"""The URL of the specific search result.""" - title: NotRequired[str] - r"""The title or name of the search result.""" - description: NotRequired[str] - r"""A brief description of the content of the search result.""" - snippets: NotRequired[List[str]] - r"""An array of text snippets from the search result, providing a preview of the content.""" - thumbnail_url: NotRequired[str] - r"""URL of the thumbnail.""" - page_age: NotRequired[datetime] - r"""The age of the search result.""" - contents: NotRequired[ContentsTypedDict] - r"""Contents of the page if livecrawl was enabled.""" - authors: NotRequired[List[str]] - r"""An array of authors of the search result.""" - favicon_url: NotRequired[str] - r"""The URL of the favicon of the search result's domain.""" - - -class Web(BaseModel): - url: Optional[str] = None - r"""The URL of the specific search result.""" - - title: Optional[str] = None - r"""The title or name of the search result.""" - - description: Optional[str] = None - r"""A brief description of the content of the search result.""" - - snippets: Optional[List[str]] = None - r"""An array of text snippets from the search result, providing a preview of the content.""" - - thumbnail_url: Optional[str] = None - r"""URL of the thumbnail.""" - - page_age: Optional[datetime] = None - r"""The age of the search result.""" - - contents: Optional[Contents] = None - r"""Contents of the page if livecrawl was enabled.""" - - authors: Optional[List[str]] = None - r"""An array of authors of the search result.""" - - favicon_url: Optional[str] = None - r"""The URL of the favicon of the search result's domain.""" - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set( - [ - "url", - "title", - "description", - "snippets", - "thumbnail_url", - "page_age", - "contents", - "authors", - "favicon_url", - ] - ) - serialized = handler(self) - m = {} - - for n, f in type(self).model_fields.items(): - k = f.alias or n - val = serialized.get(k) - - if val != UNSET_SENTINEL: - if val is not None or k not in optional_fields: - m[k] = val - - return m - - -class NewsTypedDict(TypedDict): - title: NotRequired[str] - r"""The title of the news result.""" - description: NotRequired[str] - r"""A brief description of the content of the news result.""" - page_age: NotRequired[datetime] - r"""UTC timestamp of the article's publication date.""" - thumbnail_url: NotRequired[str] - r"""URL of the thumbnail.""" - url: NotRequired[str] - r"""The URL of the news result.""" - contents: NotRequired[ContentsTypedDict] - r"""Contents of the page if livecrawl was enabled.""" - - -class News(BaseModel): - title: Optional[str] = None - r"""The title of the news result.""" - - description: Optional[str] = None - r"""A brief description of the content of the news result.""" - - page_age: Optional[datetime] = None - r"""UTC timestamp of the article's publication date.""" - - thumbnail_url: Optional[str] = None - r"""URL of the thumbnail.""" - - url: Optional[str] = None - r"""The URL of the news result.""" - - contents: Optional[Contents] = None - r"""Contents of the page if livecrawl was enabled.""" - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set( - ["title", "description", "page_age", "thumbnail_url", "url", "contents"] - ) - serialized = handler(self) - m = {} - - for n, f in type(self).model_fields.items(): - k = f.alias or n - val = serialized.get(k) - - if val != UNSET_SENTINEL: - if val is not None or k not in optional_fields: - m[k] = val - - return m - - -class ResultsTypedDict(TypedDict): - web: NotRequired[List[WebTypedDict]] - news: NotRequired[List[NewsTypedDict]] - - -class Results(BaseModel): - web: Optional[List[Web]] = None - - news: Optional[List[News]] = None - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["web", "news"]) - serialized = handler(self) - m = {} - - for n, f in type(self).model_fields.items(): - k = f.alias or n - val = serialized.get(k) - - if val != UNSET_SENTINEL: - if val is not None or k not in optional_fields: - m[k] = val - - return m - - -class MetadataTypedDict(TypedDict): - search_uuid: NotRequired[str] - query: NotRequired[str] - r"""Returns the search query used to retrieve the results.""" - latency: NotRequired[float] - - -class Metadata(BaseModel): - search_uuid: Optional[str] = None - - query: Optional[str] = None - r"""Returns the search query used to retrieve the results.""" - - latency: Optional[float] = None - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["search_uuid", "query", "latency"]) - serialized = handler(self) - m = {} - - for n, f in type(self).model_fields.items(): - k = f.alias or n - val = serialized.get(k) - - if val != UNSET_SENTINEL: - if val is not None or k not in optional_fields: - m[k] = val - - return m - - -class SearchResponseTypedDict(TypedDict): - r"""A JSON object containing unified search results from web and news sources""" - - results: NotRequired[ResultsTypedDict] - metadata: NotRequired[MetadataTypedDict] - - -class SearchResponse(BaseModel): - r"""A JSON object containing unified search results from web and news sources""" - - results: Optional[Results] = None - - metadata: Optional[Metadata] = None - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["results", "metadata"]) - serialized = handler(self) - m = {} - - for n, f in type(self).model_fields.items(): - k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/searchpostop.py b/src/youdotcom/models/searchpostop.py new file mode 100644 index 0000000..e0f8b5b --- /dev/null +++ b/src/youdotcom/models/searchpostop.py @@ -0,0 +1,8 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations + + +SEARCH_POST_OP_SERVERS = [ + "https://ydc-index.io", +] diff --git a/src/youdotcom/models/searchrequestbody.py b/src/youdotcom/models/searchrequestbody.py new file mode 100644 index 0000000..df94c4c --- /dev/null +++ b/src/youdotcom/models/searchrequestbody.py @@ -0,0 +1,132 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .country import Country +from .freshnessvalue import FreshnessValue, FreshnessValueTypedDict +from .language import Language +from .livecrawl import LiveCrawl +from .livecrawlformats import LiveCrawlFormats +from .safesearch import SafeSearch +from pydantic import model_serializer +from typing import List, Optional +from typing_extensions import NotRequired, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class SearchRequestBodyTypedDict(TypedDict): + query: str + r"""The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search.""" + count: NotRequired[int] + r"""Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them).""" + freshness: NotRequired[FreshnessValueTypedDict] + r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. + """ + offset: NotRequired[int] + r"""Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`.""" + country: NotRequired[Country] + r"""The country code that determines the geographical focus of the web results.""" + language: NotRequired[Language] + r"""The language of the web results that will be returned (BCP 47 format).""" + safesearch: NotRequired[SafeSearch] + r"""Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not.""" + livecrawl: NotRequired[LiveCrawl] + r"""Indicates which section(s) of search results to livecrawl and return full page content.""" + livecrawl_formats: NotRequired[List[LiveCrawlFormats]] + r"""Indicates the format(s) of the livecrawled content. Pass one or both values (`html`, `markdown`). In a GET request, repeat the parameter: `?livecrawl_formats=html&livecrawl_formats=markdown`. In a POST body, provide a JSON array: `[\"html\", \"markdown\"]`.""" + include_domains: NotRequired[List[str]] + r"""A list of domains to restrict search results to. Only results from these domains will be returned. Supports up to 500 domains. This is a strict allowlist, not a boost — results are limited exclusively to the specified domains. + + Cannot be combined with `exclude_domains`; passing both will return a `422` error. + """ + exclude_domains: NotRequired[List[str]] + r"""A list of domains to exclude from search results. Results from these domains will be filtered out. Supports up to 500 domains. + + Cannot be combined with `include_domains`; passing both will return a `422` error. + """ + boost_domains: NotRequired[List[str]] + r"""A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`).""" + crawl_timeout: NotRequired[int] + r"""Maximum time in seconds to wait for page content when `livecrawl` is enabled. Must be between 1 and 60 seconds. Default is 10 seconds.""" + + +class SearchRequestBody(BaseModel): + query: str + r"""The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search.""" + + count: Optional[int] = 10 + r"""Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them).""" + + freshness: Optional[FreshnessValue] = None + r"""Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. + """ + + offset: Optional[int] = None + r"""Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`.""" + + country: Optional[Country] = None + r"""The country code that determines the geographical focus of the web results.""" + + language: Optional[Language] = Language.EN + r"""The language of the web results that will be returned (BCP 47 format).""" + + safesearch: Optional[SafeSearch] = None + r"""Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not.""" + + livecrawl: Optional[LiveCrawl] = None + r"""Indicates which section(s) of search results to livecrawl and return full page content.""" + + livecrawl_formats: Optional[List[LiveCrawlFormats]] = None + r"""Indicates the format(s) of the livecrawled content. Pass one or both values (`html`, `markdown`). In a GET request, repeat the parameter: `?livecrawl_formats=html&livecrawl_formats=markdown`. In a POST body, provide a JSON array: `[\"html\", \"markdown\"]`.""" + + include_domains: Optional[List[str]] = None + r"""A list of domains to restrict search results to. Only results from these domains will be returned. Supports up to 500 domains. This is a strict allowlist, not a boost — results are limited exclusively to the specified domains. + + Cannot be combined with `exclude_domains`; passing both will return a `422` error. + """ + + exclude_domains: Optional[List[str]] = None + r"""A list of domains to exclude from search results. Results from these domains will be filtered out. Supports up to 500 domains. + + Cannot be combined with `include_domains`; passing both will return a `422` error. + """ + + boost_domains: Optional[List[str]] = None + r"""A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`).""" + + crawl_timeout: Optional[int] = 10 + r"""Maximum time in seconds to wait for page content when `livecrawl` is enabled. Must be between 1 and 60 seconds. Default is 10 seconds.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + [ + "count", + "freshness", + "offset", + "country", + "language", + "safesearch", + "livecrawl", + "livecrawl_formats", + "include_domains", + "exclude_domains", + "boost_domains", + "crawl_timeout", + ] + ) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/src/youdotcom/models/searchresponse.py b/src/youdotcom/models/searchresponse.py new file mode 100644 index 0000000..ee9313e --- /dev/null +++ b/src/youdotcom/models/searchresponse.py @@ -0,0 +1,68 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .newsresult import NewsResult, NewsResultTypedDict +from .searchmetadata import SearchMetadata, SearchMetadataTypedDict +from .webresult import WebResult, WebResultTypedDict +from pydantic import model_serializer +from typing import List, Optional +from typing_extensions import NotRequired, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class ResultsTypedDict(TypedDict): + web: NotRequired[List[WebResultTypedDict]] + news: NotRequired[List[NewsResultTypedDict]] + + +class Results(BaseModel): + web: Optional[List[WebResult]] = None + + news: Optional[List[NewsResult]] = None + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["web", "news"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +class SearchResponseTypedDict(TypedDict): + r"""A JSON object containing unified search results from web and news sources""" + + results: NotRequired[ResultsTypedDict] + metadata: NotRequired[SearchMetadataTypedDict] + + +class SearchResponse(BaseModel): + r"""A JSON object containing unified search results from web and news sources""" + + results: Optional[Results] = None + + metadata: Optional[SearchMetadata] = None + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["results", "metadata"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/src/youdotcom/models/security.py b/src/youdotcom/models/security.py index 7778995..2313b52 100644 --- a/src/youdotcom/models/security.py +++ b/src/youdotcom/models/security.py @@ -33,7 +33,7 @@ def serialize_model(self, handler): for n, f in type(self).model_fields.items(): k = f.alias or n - val = serialized.get(k) + val = serialized.get(k, serialized.get(n)) if val != UNSET_SENTINEL: if val is not None or k not in optional_fields: diff --git a/src/youdotcom/models/streamresearchtaskop.py b/src/youdotcom/models/streamresearchtaskop.py new file mode 100644 index 0000000..6b57b1d --- /dev/null +++ b/src/youdotcom/models/streamresearchtaskop.py @@ -0,0 +1,44 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from pydantic import model_serializer +from typing import Optional +from typing_extensions import Annotated, NotRequired, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL +from youdotcom.utils import FieldMetadata, PathParamMetadata, QueryParamMetadata + + +class StreamResearchTaskRequestTypedDict(TypedDict): + task_id: str + r"""The UUID of the research task.""" + from_id: NotRequired[int] + r"""Resume from a sequence number for reconnection.""" + + +class StreamResearchTaskRequest(BaseModel): + task_id: Annotated[ + str, FieldMetadata(path=PathParamMetadata(style="simple", explode=False)) + ] + r"""The UUID of the research task.""" + + from_id: Annotated[ + Optional[int], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = 0 + r"""Resume from a sequence number for reconnection.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["from_id"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/src/youdotcom/models/taskdetail.py b/src/youdotcom/models/taskdetail.py new file mode 100644 index 0000000..568ca9f --- /dev/null +++ b/src/youdotcom/models/taskdetail.py @@ -0,0 +1,109 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from datetime import datetime +from enum import Enum +from pydantic import model_serializer +from typing_extensions import NotRequired, TypedDict +from youdotcom.types import BaseModel, Nullable, OptionalNullable, UNSET, UNSET_SENTINEL + + +class TaskDetailStatus(str, Enum): + r"""Current status of the task.""" + + QUEUED = "queued" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class TaskDetailInputTypedDict(TypedDict): + r"""The original request input for the task.""" + + +class TaskDetailInput(BaseModel): + r"""The original request input for the task.""" + + +class ResultTypedDict(TypedDict): + r"""The task result when completed. For research tasks, this contains the ResearchResponse output.""" + + +class Result(BaseModel): + r"""The task result when completed. For research tasks, this contains the ResearchResponse output.""" + + +class TaskDetailTypedDict(TypedDict): + id: str + r"""Unique identifier for the task.""" + task_type: str + r"""Task type.""" + status: TaskDetailStatus + r"""Current status of the task.""" + created_at: datetime + r"""When the task was created.""" + updated_at: datetime + r"""When the task was last updated.""" + completed_at: NotRequired[Nullable[datetime]] + r"""When the task completed, if applicable.""" + error: NotRequired[Nullable[str]] + r"""Error message if the task failed.""" + input: NotRequired[Nullable[TaskDetailInputTypedDict]] + r"""The original request input for the task.""" + result: NotRequired[Nullable[ResultTypedDict]] + r"""The task result when completed. For research tasks, this contains the ResearchResponse output.""" + + +class TaskDetail(BaseModel): + id: str + r"""Unique identifier for the task.""" + + task_type: str + r"""Task type.""" + + status: TaskDetailStatus + r"""Current status of the task.""" + + created_at: datetime + r"""When the task was created.""" + + updated_at: datetime + r"""When the task was last updated.""" + + completed_at: OptionalNullable[datetime] = UNSET + r"""When the task completed, if applicable.""" + + error: OptionalNullable[str] = UNSET + r"""Error message if the task failed.""" + + input: OptionalNullable[TaskDetailInput] = UNSET + r"""The original request input for the task.""" + + result: OptionalNullable[Result] = UNSET + r"""The task result when completed. For research tasks, this contains the ResearchResponse output.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["completed_at", "error", "input", "result"]) + nullable_fields = set(["completed_at", "error", "input", "result"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + is_nullable_and_explicitly_set = ( + k in nullable_fields + and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member + ) + + if val != UNSET_SENTINEL: + if ( + val is not None + or k not in optional_fields + or is_nullable_and_explicitly_set + ): + m[k] = val + + return m diff --git a/src/youdotcom/models/taskresponse.py b/src/youdotcom/models/taskresponse.py new file mode 100644 index 0000000..490476d --- /dev/null +++ b/src/youdotcom/models/taskresponse.py @@ -0,0 +1,47 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from datetime import datetime +from enum import Enum +from typing_extensions import TypedDict +from youdotcom.types import BaseModel + + +class TaskResponseStatus(str, Enum): + r"""Current status of the task.""" + + QUEUED = "queued" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class TaskResponseTypedDict(TypedDict): + task_id: str + r"""Unique identifier for the task.""" + type: str + r"""Task type.""" + status: TaskResponseStatus + r"""Current status of the task.""" + stream_url: str + r"""URL to stream task events via SSE.""" + created_at: datetime + r"""When the task was created.""" + + +class TaskResponse(BaseModel): + task_id: str + r"""Unique identifier for the task.""" + + type: str + r"""Task type.""" + + status: TaskResponseStatus + r"""Current status of the task.""" + + stream_url: str + r"""URL to stream task events via SSE.""" + + created_at: datetime + r"""When the task was created.""" diff --git a/src/youdotcom/models/webresult.py b/src/youdotcom/models/webresult.py new file mode 100644 index 0000000..d8f6f6c --- /dev/null +++ b/src/youdotcom/models/webresult.py @@ -0,0 +1,87 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .contents import Contents, ContentsTypedDict +from datetime import datetime +from pydantic import model_serializer +from typing import List, Optional +from typing_extensions import NotRequired, TypedDict +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class WebResultTypedDict(TypedDict): + url: NotRequired[str] + r"""The URL of the specific search result.""" + title: NotRequired[str] + r"""The title or name of the search result.""" + description: NotRequired[str] + r"""A brief description of the content of the search result.""" + snippets: NotRequired[List[str]] + r"""An array of text snippets from the search result, providing a preview of the content.""" + thumbnail_url: NotRequired[str] + r"""URL of the thumbnail.""" + page_age: NotRequired[datetime] + r"""The age of the search result.""" + contents: NotRequired[ContentsTypedDict] + r"""Contents of the page if livecrawl was enabled.""" + authors: NotRequired[List[str]] + r"""An array of authors of the search result.""" + favicon_url: NotRequired[str] + r"""The URL of the favicon of the search result's domain.""" + + +class WebResult(BaseModel): + url: Optional[str] = None + r"""The URL of the specific search result.""" + + title: Optional[str] = None + r"""The title or name of the search result.""" + + description: Optional[str] = None + r"""A brief description of the content of the search result.""" + + snippets: Optional[List[str]] = None + r"""An array of text snippets from the search result, providing a preview of the content.""" + + thumbnail_url: Optional[str] = None + r"""URL of the thumbnail.""" + + page_age: Optional[datetime] = None + r"""The age of the search result.""" + + contents: Optional[Contents] = None + r"""Contents of the page if livecrawl was enabled.""" + + authors: Optional[List[str]] = None + r"""An array of authors of the search result.""" + + favicon_url: Optional[str] = None + r"""The URL of the favicon of the search result's domain.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + [ + "url", + "title", + "description", + "snippets", + "thumbnail_url", + "page_age", + "contents", + "authors", + "favicon_url", + ] + ) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/src/youdotcom/research_helpers.py b/src/youdotcom/research_helpers.py new file mode 100644 index 0000000..c863c3c --- /dev/null +++ b/src/youdotcom/research_helpers.py @@ -0,0 +1,492 @@ +"""Hand-maintained research workflow helpers. + +This module is NOT regenerated by Speakeasy. It adds convenience helpers on +top of the auto-generated research endpoints: + +- ``research_background`` / ``research_background_async``: Submit a research + task with ``background=True`` and return the ``TaskResponse`` directly so + callers don't need to narrow the ``Union[ResearchResponse, TaskResponse]`` + response shape. +- ``poll_research_task`` / ``poll_research_task_async``: Poll + ``GET /v1/research/{task_id}`` until the task reaches a terminal state + (``completed``, ``failed``, ``cancelled``). +- ``research_and_wait`` / ``research_and_wait_async``: Submit a research task + with ``background=True``, then either poll or stream until completion and + return the final ``TaskDetail``. +- ``stream_research_events_raw`` / ``stream_research_events_raw_async``: + Iterate the SSE event stream with a tolerant decoder that surfaces + non-typed event names (``research.searching``, etc.) as raw dicts instead + of crashing on validation. Use this when the server may emit event types + outside the documented ``connected``/``response.done``/``complete``/ + ``error``/``cancelled`` set. +""" + +from __future__ import annotations + +import asyncio +import json +import time +from dataclasses import dataclass +from typing import Any, AsyncIterator, Iterator, Mapping, Optional + +import youdotcom.models as _models +from youdotcom._hooks.types import HookContext +from youdotcom.sdk import You +from youdotcom.models import TaskDetail, TaskResponse +from youdotcom.utils import eventstreaming, get_security_from_env, match_response, match_status_codes + + +_DEFAULT_POLL_INTERVAL_S = 2.0 +_DEFAULT_POLL_TIMEOUT_S = 600.0 +_TERMINAL_TASK_STATUSES = frozenset({"completed", "failed", "cancelled"}) + + +@dataclass +class RawStreamEvent: + """Tolerant SSE event representation. + + Attributes: + id: SSE event ID (``id:`` directive) when supplied. + event: Raw event name as received from the server, NOT validated + against the documented enum. + data: Parsed JSON payload when the data line is valid JSON, + otherwise the raw string. + retry: Optional ``retry:`` directive from the server, if any. + """ + + id: Optional[str] + event: Optional[str] + data: Any + retry: Optional[int] + + +def _decode_raw_event(raw_json: str) -> RawStreamEvent: + """Tolerant SSE decoder used by ``stream_research_events_raw``. + + Accepts any JSON object regardless of whether ``event`` matches the + documented enum, so unknown workflow events pass through instead of + failing pydantic validation. + """ + parsed = json.loads(raw_json) + if not isinstance(parsed, dict): + return RawStreamEvent(id=None, event=None, data=parsed, retry=None) + return RawStreamEvent( + id=parsed.get("id"), + event=parsed.get("event"), + data=parsed.get("data"), + retry=parsed.get("retry"), + ) + + +def research_background(client: You, **kwargs: Any) -> TaskResponse: + """Submit a research task with ``background=True`` and return ``TaskResponse``. + + Equivalent to ``client.research(..., background=True)`` but asserts the + response is a ``TaskResponse`` so callers get a strong return type + without needing to narrow ``Union[ResearchResponse, TaskResponse]``. + """ + kw = dict(kwargs) + kw["background"] = True + res = client.research(**kw) + if not isinstance(res, TaskResponse): + raise TypeError( + "research_background requires background=True so the server " + "returns TaskResponse; got " + type(res).__name__ + ) + return res + + +async def research_background_async(client: You, **kwargs: Any) -> TaskResponse: + """Async variant of :func:`research_background`.""" + kw = dict(kwargs) + kw["background"] = True + res = await client.research_async(**kw) + if not isinstance(res, TaskResponse): + raise TypeError( + "research_background_async requires background=True so the " + "server returns TaskResponse; got " + type(res).__name__ + ) + return res + + +def _poll_detail( + client: You, + *, + task_id: str, + interval_s: float, + timeout_s: float, + deadline: float, +) -> TaskDetail: + while True: + detail = client.get_research_task(task_id=task_id) + status = getattr(detail.status, "value", detail.status) + if status in _TERMINAL_TASK_STATUSES: + if status != "completed": + raise RuntimeError( + f"research task {task_id} ended in non-completed state: {status}" + ) + return detail + if time.monotonic() >= deadline: + raise TimeoutError( + f"research task {task_id} did not complete within {timeout_s}s" + ) + time.sleep(interval_s) + + +def poll_research_task( + client: You, + task_id: str, + *, + interval_s: float = _DEFAULT_POLL_INTERVAL_S, + timeout_s: float = _DEFAULT_POLL_TIMEOUT_S, +) -> TaskDetail: + """Poll ``GET /v1/research/{task_id}`` until ``status == "completed"``. + + Raises ``RuntimeError`` if the task ends in a non-completed terminal state + (``failed`` / ``cancelled``) and ``TimeoutError`` if ``timeout_s`` elapses + before completion. + """ + return _poll_detail( + client, + task_id=task_id, + interval_s=interval_s, + timeout_s=timeout_s, + deadline=time.monotonic() + timeout_s, + ) + + +async def _poll_detail_async( + client: You, + *, + task_id: str, + interval_s: float, + timeout_s: float, + deadline: float, +) -> TaskDetail: + while True: + detail = await client.get_research_task_async(task_id=task_id) + status = getattr(detail.status, "value", detail.status) + if status in _TERMINAL_TASK_STATUSES: + if status != "completed": + raise RuntimeError( + f"research task {task_id} ended in non-completed state: {status}" + ) + return detail + if time.monotonic() >= deadline: + raise TimeoutError( + f"research task {task_id} did not complete within {timeout_s}s" + ) + await asyncio.sleep(interval_s) + + +async def poll_research_task_async( + client: You, + task_id: str, + *, + interval_s: float = _DEFAULT_POLL_INTERVAL_S, + timeout_s: float = _DEFAULT_POLL_TIMEOUT_S, +) -> TaskDetail: + """Async variant of :func:`poll_research_task`.""" + return await _poll_detail_async( + client, + task_id=task_id, + interval_s=interval_s, + timeout_s=timeout_s, + deadline=time.monotonic() + timeout_s, + ) + + +def research_and_wait( + client: You, + *, + mode: str = "poll", + interval_s: float = _DEFAULT_POLL_INTERVAL_S, + timeout_s: float = _DEFAULT_POLL_TIMEOUT_S, + **kwargs: Any, +) -> TaskDetail: + """Submit a research task in background mode and wait for completion. + + Parameters: + mode: ``"poll"`` (default) or ``"stream"``. ``stream`` reads the SSE + stream until a terminal ``response.done`` event arrives, then + fetches the final ``TaskDetail`` for the structured payload. + interval_s: Poll interval in seconds (poll mode only). + timeout_s: Maximum seconds to wait for completion. + ``**kwargs``: Forwarded to ``client.research``, including + ``input`` and ``research_effort``. ``background=True`` is set + automatically. + + Returns: + ``TaskDetail`` with ``status == completed`` and the task's + ``result`` attribute populated. Note: due to current SDK + unmarshaling (``extra=ignore`` on the generic ``Result`` model + which declares no fields), the inline ``ResearchResponse`` + payload in ``detail.result`` is not typed today — + ``detail.result.model_dump()`` returns an empty dict because + pydantic drops the unknown ``output`` keys. The supported + workaround is to issue a follow-up synchronous + ``client.research(..., background=False)`` call with the same + ``input`` and ``research_effort`` to get a typed + ``ResearchResponse``. + """ + if mode not in {"poll", "stream"}: + raise ValueError(f"mode must be 'poll' or 'stream', got {mode!r}") + task = research_background(client, **kwargs) + if mode == "poll": + return poll_research_task( + client, + task.task_id, + interval_s=interval_s, + timeout_s=timeout_s, + ) + deadline = time.monotonic() + timeout_s + # Stream mode: poll until terminal event OR deadline, then fetch the + # final detail. The deadline check below runs after each received event; + # if the SSE connection itself stalls (server sends nothing), the real + # backstop is the httpx read timeout on the underlying request, not + # ``timeout_s``. + for evt in stream_research_events_raw(client, task.task_id): + name = evt.event + if name in {"response.done", "complete"}: + return poll_research_task( + client, + task.task_id, + interval_s=interval_s, + timeout_s=max(interval_s, deadline - time.monotonic()), + ) + if name in {"error", "cancelled"}: + raise RuntimeError( + f"research task {task.task_id} ended in non-completed state: {name}" + ) + if time.monotonic() >= deadline: + raise TimeoutError( + f"research task {task.task_id} did not complete within {timeout_s}s" + ) + # Stream ended without a terminal event; fall back to polling. + return poll_research_task( + client, + task.task_id, + interval_s=interval_s, + timeout_s=max(interval_s, deadline - time.monotonic()), + ) + + +async def research_and_wait_async( + client: You, + *, + mode: str = "poll", + interval_s: float = _DEFAULT_POLL_INTERVAL_S, + timeout_s: float = _DEFAULT_POLL_TIMEOUT_S, + **kwargs: Any, +) -> TaskDetail: + """Async variant of :func:`research_and_wait`.""" + if mode not in {"poll", "stream"}: + raise ValueError(f"mode must be 'poll' or 'stream', got {mode!r}") + task = await research_background_async(client, **kwargs) + if mode == "poll": + return await poll_research_task_async( + client, + task.task_id, + interval_s=interval_s, + timeout_s=timeout_s, + ) + deadline = time.monotonic() + timeout_s + # Stream mode: poll until terminal event OR deadline, then fetch the + # final detail. The deadline check below runs after each received event; + # if the SSE connection itself stalls (server sends nothing), the real + # backstop is the httpx read timeout on the underlying request, not + # ``timeout_s``. + 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()), + ) + if name in {"error", "cancelled"}: + raise RuntimeError( + f"research task {task.task_id} ended in non-completed state: {name}" + ) + if time.monotonic() >= deadline: + raise TimeoutError( + f"research task {task.task_id} did not complete within {timeout_s}s" + ) + # Stream ended without a terminal event; fall back to polling. + return await poll_research_task_async( + client, + task.task_id, + interval_s=interval_s, + timeout_s=max(interval_s, deadline - time.monotonic()), + ) + + +def _open_raw_stream( + client: You, + task_id: str, + *, + http_headers: Optional[Mapping[str, str]], + from_id: int = 0, +) -> eventstreaming.EventStream[RawStreamEvent]: + """Issue the SSE request directly so we can pass a tolerant decoder. + + Mirrors the request setup used by ``client.stream_research_task`` but + wires :func:`_decode_raw_event` into the ``EventStream`` instead of the + strict pydantic ``ResearchTaskStreamEvent`` decoder. + """ + # pylint: disable=protected-access + base_url = client._get_url(None, None) + timeout_ms = client.sdk_configuration.timeout_ms + req = client._build_request( + method="GET", + path="/v1/research/{task_id}/stream", + base_url=base_url, + url_variables=None, + request=_models.StreamResearchTaskRequest(task_id=task_id, from_id=from_id), + request_body_required=False, + request_has_path_params=True, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="text/event-stream", + http_headers=http_headers, + security=client.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + http_res = client.do_request( + hook_ctx=HookContext( + config=client.sdk_configuration, + base_url=base_url or "", + operation_id="streamResearchTask", + oauth2_scopes=None, + security_source=get_security_from_env( + client.sdk_configuration.security, _models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: match_status_codes(["4XX", "5XX"], c), + stream=True, + retry_config=None, + ) + if not match_response(http_res, "200", "text/event-stream"): + http_res.close() + raise RuntimeError( + f"unexpected response from research stream: {http_res.status_code}" + ) + return eventstreaming.EventStream( + http_res, + _decode_raw_event, + client_ref=client, + data_required=False, + ) + + +def stream_research_events_raw( + client: You, + task_id: str, + *, + http_headers: Optional[Mapping[str, str]] = None, + from_id: int = 0, +) -> Iterator[RawStreamEvent]: + """SSE iterator that accepts unknown event names without failing. + + Each yielded item is a :class:`RawStreamEvent` carrying the raw ``event`` + string and parsed JSON ``data`` payload. Useful when the server may emit + intermediate workflow events with names outside the documented enum + (``connected``/``response.done``/``complete``/``error``/``cancelled``). + + Parameters: + from_id: Sequence number to resume from (reconnection). ``0`` starts at + the beginning of the stream. + """ + # pylint: disable-next=protected-access + with _open_raw_stream( + client, task_id, http_headers=http_headers, from_id=from_id, + ) as stream: + yield from stream + + +async def _open_raw_stream_async( + client: You, + task_id: str, + *, + http_headers: Optional[Mapping[str, str]], + from_id: int = 0, +) -> eventstreaming.EventStreamAsync[RawStreamEvent]: + """Async counterpart of :func:`_open_raw_stream`.""" + # pylint: disable=protected-access + base_url = client._get_url(None, None) + timeout_ms = client.sdk_configuration.timeout_ms + req = client._build_request_async( + method="GET", + path="/v1/research/{task_id}/stream", + base_url=base_url, + url_variables=None, + request=_models.StreamResearchTaskRequest(task_id=task_id, from_id=from_id), + request_body_required=False, + request_has_path_params=True, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="text/event-stream", + http_headers=http_headers, + security=client.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + http_res = await client.do_request_async( + hook_ctx=HookContext( + config=client.sdk_configuration, + base_url=base_url or "", + operation_id="streamResearchTask", + oauth2_scopes=None, + security_source=get_security_from_env( + client.sdk_configuration.security, _models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: match_status_codes(["4XX", "5XX"], c), + stream=True, + retry_config=None, + ) + if not match_response(http_res, "200", "text/event-stream"): + await http_res.aclose() + raise RuntimeError( + f"unexpected response from research stream: {http_res.status_code}" + ) + return eventstreaming.EventStreamAsync( + http_res, + _decode_raw_event, + client_ref=client, + data_required=False, + ) + + +async def stream_research_events_raw_async( + client: You, + task_id: str, + *, + http_headers: Optional[Mapping[str, str]] = None, + from_id: int = 0, +) -> AsyncIterator[RawStreamEvent]: + """Async variant of :func:`stream_research_events_raw`. + + Parameters: + from_id: Sequence number to resume from (reconnection). ``0`` starts at + the beginning of the stream. + """ + # 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() diff --git a/src/youdotcom/runs.py b/src/youdotcom/runs.py index 0fc5917..0c63569 100644 --- a/src/youdotcom/runs.py +++ b/src/youdotcom/runs.py @@ -1,7 +1,6 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" from .basesdk import BaseSDK -from enum import Enum from typing import Any, Mapping, Optional, Union, cast from youdotcom import errors, models, utils from youdotcom._hooks import HookContext @@ -10,11 +9,6 @@ from youdotcom.utils.unmarshal_json_response import unmarshal_json_response -class CreateAcceptEnum(str, Enum): - APPLICATION_JSON = "application/json" - TEXT_EVENT_STREAM = "text/event-stream" - - class Runs(BaseSDK): def create( self, @@ -23,9 +17,11 @@ def create( retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, - accept_header_override: Optional[CreateAcceptEnum] = None, http_headers: Optional[Mapping[str, str]] = None, - ) -> models.AgentsRunsResponse: + ) -> Union[ + models.AgentRunsBatchResponse, + eventstreaming.EventStream[models.AgentRunsStreamingResponse], + ]: r"""Run an Agent Execute queries using You.com's AI agents. This endpoint supports three agent types: @@ -41,7 +37,6 @@ def create( :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds - :param accept_header_override: Override the default accept header for this method :param http_headers: Additional headers to set or replace on requests. """ base_url = None @@ -52,7 +47,7 @@ def create( if server_url is not None: base_url = server_url else: - base_url = self._get_url(base_url, url_variables) + base_url = models.AGENTS_RUNS_OP_SERVERS[0] if not isinstance(request, BaseModel): request = utils.unmarshal(request, models.AgentsRunsRequest) @@ -68,9 +63,9 @@ def create( request_has_path_params=False, request_has_query_params=True, user_agent_header="user-agent", - accept_header_value=accept_header_override.value - if accept_header_override is not None - else "application/json;q=1, text/event-stream;q=0", + accept_header_value="text/event-stream" + if getattr(request, "stream", False) is True + else "application/json", http_headers=http_headers, security=self.sdk_configuration.security, get_serialized_body=lambda: utils.serialize_request_body( @@ -97,10 +92,12 @@ def create( security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), + tags=["agents.runs"], + extensions=None, ), request=req, - error_status_codes=["400", "401", "422", "4XX", "5XX"], - stream=True, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + stream=getattr(request, "stream", False) is True, retry_config=retry_config, ) @@ -113,8 +110,8 @@ def create( if utils.match_response(http_res, "200", "text/event-stream"): return eventstreaming.EventStream( http_res, - lambda raw: utils.unmarshal_json( - raw, models.AgentRunsStreamingResponse + lambda raw: unmarshal_json_response( + models.AgentRunsStreamingResponse, http_res, raw ), client_ref=self, ) @@ -161,9 +158,11 @@ async def create_async( retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, - accept_header_override: Optional[CreateAcceptEnum] = None, http_headers: Optional[Mapping[str, str]] = None, - ) -> models.AgentsRunsResponse: + ) -> Union[ + models.AgentRunsBatchResponse, + eventstreaming.EventStreamAsync[models.AgentRunsStreamingResponse], + ]: r"""Run an Agent Execute queries using You.com's AI agents. This endpoint supports three agent types: @@ -179,7 +178,6 @@ async def create_async( :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds - :param accept_header_override: Override the default accept header for this method :param http_headers: Additional headers to set or replace on requests. """ base_url = None @@ -190,7 +188,7 @@ async def create_async( if server_url is not None: base_url = server_url else: - base_url = self._get_url(base_url, url_variables) + base_url = models.AGENTS_RUNS_OP_SERVERS[0] if not isinstance(request, BaseModel): request = utils.unmarshal(request, models.AgentsRunsRequest) @@ -206,9 +204,9 @@ async def create_async( request_has_path_params=False, request_has_query_params=True, user_agent_header="user-agent", - accept_header_value=accept_header_override.value - if accept_header_override is not None - else "application/json;q=1, text/event-stream;q=0", + accept_header_value="text/event-stream" + if getattr(request, "stream", False) is True + else "application/json", http_headers=http_headers, security=self.sdk_configuration.security, get_serialized_body=lambda: utils.serialize_request_body( @@ -235,10 +233,12 @@ async def create_async( security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), + tags=["agents.runs"], + extensions=None, ), request=req, - error_status_codes=["400", "401", "422", "4XX", "5XX"], - stream=True, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + stream=getattr(request, "stream", False) is True, retry_config=retry_config, ) @@ -251,8 +251,8 @@ async def create_async( if utils.match_response(http_res, "200", "text/event-stream"): return eventstreaming.EventStreamAsync( http_res, - lambda raw: utils.unmarshal_json( - raw, models.AgentRunsStreamingResponse + lambda raw: unmarshal_json_response( + models.AgentRunsStreamingResponse, http_res, raw ), client_ref=self, ) diff --git a/src/youdotcom/sdk.py b/src/youdotcom/sdk.py index c72bef1..67696cc 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -8,12 +8,23 @@ import httpx import importlib import sys -from typing import Any, Callable, Dict, Mapping, Optional, TYPE_CHECKING, Union, cast +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + Optional, + TYPE_CHECKING, + Union, + cast, +) import weakref from youdotcom import errors, models, utils from youdotcom._hooks import HookContext, SDKHooks from youdotcom.types import OptionalNullable, UNSET -from youdotcom.utils import get_security_from_env +from youdotcom.utils import eventstreaming, get_security_from_env from youdotcom.utils.unmarshal_json_response import unmarshal_json_response if TYPE_CHECKING: @@ -23,14 +34,17 @@ class You(BaseSDK): - r"""You.com API: Unified API for Express, Advanced, and Custom Agents from You.com + r"""You.com Finance Research API: Unified API for Express, Advanced, and Custom Agents from You.com Get the best search results from web and news sources Returns the HTML or Markdown of a target webpage - Multi-step reasoning with comprehensive research capabilities Comprehensive API for You.com services: - **Agents API**: Execute queries using Express, Advanced, and Custom AI agents + - **Research API**: In-depth, multi-step research with citations and sources + - **Finance Research API**: Finance-focused multi-step research with citations and sources - **Search API**: Get search results from web and news sources - **Contents API**: Retrieve and process web page content + Multi-step reasoning with comprehensive research capabilities + Finance-focused multi-step research with competitive accuracy at same price points and latencies as the Research API """ agents: "Agents" @@ -89,7 +103,9 @@ def __init__( ), "The provided async_client must implement the AsyncHttpClient protocol." security: Any = None - if callable(api_key_auth): + if api_key_auth is None: + security = None + elif callable(api_key_auth): # pylint: disable=unnecessary-lambda-assignment security = lambda: models.Security(api_key_auth=api_key_auth()) else: @@ -193,6 +209,322 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): await self.sdk_configuration.async_client.aclose() self.sdk_configuration.async_client = None + def search_post( + self, + *, + query: str, + count: Optional[int] = 10, + freshness: Optional[ + Union[models.FreshnessValue, models.FreshnessValueTypedDict] + ] = None, + offset: Optional[int] = None, + country: Optional[models.Country] = None, + language: Optional[models.Language] = models.Language.EN, + safesearch: Optional[models.SafeSearch] = None, + livecrawl: Optional[models.LiveCrawl] = None, + livecrawl_formats: Optional[Iterable[models.LiveCrawlFormats]] = None, + include_domains: Optional[Iterable[str]] = None, + exclude_domains: Optional[Iterable[str]] = None, + boost_domains: Optional[Iterable[str]] = None, + crawl_timeout: Optional[int] = 10, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.SearchResponse: + r"""Returns a list of unified search results from web and news sources + + This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. + + `POST` is the recommended method when using complex parameters such as `include_domains` or `exclude_domains`. These fields accept JSON arrays in the request body, which is unambiguous and supports up to 500 domains per request—something that would exceed URL length limits with GET. Use GET for simple queries where HTTP cacheability matters. + + :param query: The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. + :param count: Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). + :param freshness: Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. + :param offset: Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. + :param country: The country code that determines the geographical focus of the web results. + :param language: The language of the web results that will be returned (BCP 47 format). + :param safesearch: Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. + :param livecrawl: Indicates which section(s) of search results to livecrawl and return full page content. + :param livecrawl_formats: Indicates the format(s) of the livecrawled content. Pass one or both values (`html`, `markdown`). In a GET request, repeat the parameter: `?livecrawl_formats=html&livecrawl_formats=markdown`. In a POST body, provide a JSON array: `[\"html\", \"markdown\"]`. + :param include_domains: A list of domains to restrict search results to. Only results from these domains will be returned. Supports up to 500 domains. This is a strict allowlist, not a boost — results are limited exclusively to the specified domains. + + Cannot be combined with `exclude_domains`; passing both will return a `422` error. + :param exclude_domains: A list of domains to exclude from search results. Results from these domains will be filtered out. Supports up to 500 domains. + + Cannot be combined with `include_domains`; passing both will return a `422` error. + :param boost_domains: A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). + :param crawl_timeout: Maximum time in seconds to wait for page content when `livecrawl` is enabled. Must be between 1 and 60 seconds. Default is 10 seconds. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = models.SEARCH_POST_OP_SERVERS[0] + + request = models.SearchRequestBody( + query=query, + count=count, + freshness=freshness, + offset=offset, + country=country, + language=language, + safesearch=safesearch, + livecrawl=livecrawl, + livecrawl_formats=utils.unmarshal( + livecrawl_formats, Optional[List[models.LiveCrawlFormats]] + ), + include_domains=utils.unmarshal(include_domains, Optional[List[str]]), + exclude_domains=utils.unmarshal(exclude_domains, Optional[List[str]]), + boost_domains=utils.unmarshal(boost_domains, Optional[List[str]]), + crawl_timeout=crawl_timeout, + ) + + req = self._build_request( + method="POST", + path="/v1/search", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=True, + request_has_path_params=False, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + security=self.sdk_configuration.security, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, False, "json", models.SearchRequestBody + ), + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="searchPost", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(models.SearchResponse, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.UnauthorizedResponseErrorData, http_res + ) + raise errors.UnauthorizedResponseError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.ForbiddenResponseErrorData, http_res + ) + raise errors.ForbiddenResponseError(response_data, http_res) + if utils.match_response(http_res, "422", "application/json"): + response_data = unmarshal_json_response( + errors.UnprocessableEntityResponseErrorData, http_res + ) + raise errors.UnprocessableEntityResponseError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.InternalServerErrorResponseData, http_res + ) + raise errors.InternalServerErrorResponse(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + raise errors.YouDefaultError("Unexpected response received", http_res) + + async def search_post_async( + self, + *, + query: str, + count: Optional[int] = 10, + freshness: Optional[ + Union[models.FreshnessValue, models.FreshnessValueTypedDict] + ] = None, + offset: Optional[int] = None, + country: Optional[models.Country] = None, + language: Optional[models.Language] = models.Language.EN, + safesearch: Optional[models.SafeSearch] = None, + livecrawl: Optional[models.LiveCrawl] = None, + livecrawl_formats: Optional[Iterable[models.LiveCrawlFormats]] = None, + include_domains: Optional[Iterable[str]] = None, + exclude_domains: Optional[Iterable[str]] = None, + boost_domains: Optional[Iterable[str]] = None, + crawl_timeout: Optional[int] = 10, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.SearchResponse: + r"""Returns a list of unified search results from web and news sources + + This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. + + `POST` is the recommended method when using complex parameters such as `include_domains` or `exclude_domains`. These fields accept JSON arrays in the request body, which is unambiguous and supports up to 500 domains per request—something that would exceed URL length limits with GET. Use GET for simple queries where HTTP cacheability matters. + + :param query: The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. + :param count: Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). + :param freshness: Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. + + When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. + :param offset: Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. + :param country: The country code that determines the geographical focus of the web results. + :param language: The language of the web results that will be returned (BCP 47 format). + :param safesearch: Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. + :param livecrawl: Indicates which section(s) of search results to livecrawl and return full page content. + :param livecrawl_formats: Indicates the format(s) of the livecrawled content. Pass one or both values (`html`, `markdown`). In a GET request, repeat the parameter: `?livecrawl_formats=html&livecrawl_formats=markdown`. In a POST body, provide a JSON array: `[\"html\", \"markdown\"]`. + :param include_domains: A list of domains to restrict search results to. Only results from these domains will be returned. Supports up to 500 domains. This is a strict allowlist, not a boost — results are limited exclusively to the specified domains. + + Cannot be combined with `exclude_domains`; passing both will return a `422` error. + :param exclude_domains: A list of domains to exclude from search results. Results from these domains will be filtered out. Supports up to 500 domains. + + Cannot be combined with `include_domains`; passing both will return a `422` error. + :param boost_domains: A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). + :param crawl_timeout: Maximum time in seconds to wait for page content when `livecrawl` is enabled. Must be between 1 and 60 seconds. Default is 10 seconds. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = models.SEARCH_POST_OP_SERVERS[0] + + request = models.SearchRequestBody( + query=query, + count=count, + freshness=freshness, + offset=offset, + country=country, + language=language, + safesearch=safesearch, + livecrawl=livecrawl, + livecrawl_formats=utils.unmarshal( + livecrawl_formats, Optional[List[models.LiveCrawlFormats]] + ), + include_domains=utils.unmarshal(include_domains, Optional[List[str]]), + exclude_domains=utils.unmarshal(exclude_domains, Optional[List[str]]), + boost_domains=utils.unmarshal(boost_domains, Optional[List[str]]), + crawl_timeout=crawl_timeout, + ) + + req = self._build_request_async( + method="POST", + path="/v1/search", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=True, + request_has_path_params=False, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + security=self.sdk_configuration.security, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, False, "json", models.SearchRequestBody + ), + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="searchPost", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(models.SearchResponse, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.UnauthorizedResponseErrorData, http_res + ) + raise errors.UnauthorizedResponseError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.ForbiddenResponseErrorData, http_res + ) + raise errors.ForbiddenResponseError(response_data, http_res) + if utils.match_response(http_res, "422", "application/json"): + response_data = unmarshal_json_response( + errors.UnprocessableEntityResponseErrorData, http_res + ) + raise errors.UnprocessableEntityResponseError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.InternalServerErrorResponseData, http_res + ) + raise errors.InternalServerErrorResponse(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + raise errors.YouDefaultError("Unexpected response received", http_res) + def research( self, *, @@ -200,11 +532,18 @@ def research( research_effort: Optional[ models.ResearchEffort ] = models.ResearchEffort.STANDARD, + background: Optional[bool] = False, + source_control: Optional[ + Union[models.SourceControl, models.SourceControlTypedDict] + ] = None, + output_schema: Optional[ + Union[models.OutputSchema, models.OutputSchemaTypedDict] + ] = None, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, http_headers: Optional[Mapping[str, str]] = None, - ) -> models.ResearchResponse: + ) -> models.ResearchResponse1: r"""Returns comprehensive research-grade answers with multi-step reasoning Research goes beyond a single web search. In response to your question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust and verify. @@ -219,6 +558,15 @@ def research( - `standard`: The default. Balances speed and depth, a good fit for most questions. - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + :param background: When true, queue a research task and return a task handle immediately instead of waiting for the result inline. Defaults to synchronous. When enabled, the response is a TaskResponse object with a task_id and stream_url for polling progress via GET /v1/research/{task_id} or streaming via GET /v1/research/{task_id}/stream. + :param source_control: Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country. + + `include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. + :param output_schema: Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: \"lite\" returns 422. + + Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported. + + Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -237,6 +585,13 @@ def research( request = models.ResearchRequest( input=input, research_effort=research_effort, + background=background, + source_control=utils.get_pydantic_model( + source_control, Optional[models.SourceControl] + ), + output_schema=utils.get_pydantic_model( + output_schema, Optional[models.OutputSchema] + ), ) req = self._build_request( @@ -276,15 +631,17 @@ def research( security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), + tags=None, + extensions=None, ), request=req, - error_status_codes=["401", "403", "422", "4XX", "500", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) response_data: Any = None if utils.match_response(http_res, "200", "application/json"): - return unmarshal_json_response(models.ResearchResponse, http_res) + return unmarshal_json_response(models.ResearchResponse1, http_res) if utils.match_response(http_res, "401", "application/json"): response_data = unmarshal_json_response( errors.ResearchUnauthorizedErrorData, http_res @@ -297,9 +654,9 @@ def research( raise errors.ResearchForbiddenError(response_data, http_res) if utils.match_response(http_res, "422", "application/json"): response_data = unmarshal_json_response( - errors.UnprocessableEntityErrorData, http_res + errors.ResearchUnprocessableEntityErrorData, http_res ) - raise errors.UnprocessableEntityError(response_data, http_res) + raise errors.ResearchUnprocessableEntityError(response_data, http_res) if utils.match_response(http_res, "500", "application/json"): response_data = unmarshal_json_response( errors.ResearchInternalServerErrorData, http_res @@ -321,11 +678,18 @@ async def research_async( research_effort: Optional[ models.ResearchEffort ] = models.ResearchEffort.STANDARD, + background: Optional[bool] = False, + source_control: Optional[ + Union[models.SourceControl, models.SourceControlTypedDict] + ] = None, + output_schema: Optional[ + Union[models.OutputSchema, models.OutputSchemaTypedDict] + ] = None, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, http_headers: Optional[Mapping[str, str]] = None, - ) -> models.ResearchResponse: + ) -> models.ResearchResponse1: r"""Returns comprehensive research-grade answers with multi-step reasoning Research goes beyond a single web search. In response to your question, it runs multiple searches, reads through the sources, and synthesizes everything into a thorough, well-cited answer. Use it when a question is too complex for a simple lookup, and when you need a response you can actually trust and verify. @@ -340,6 +704,15 @@ async def research_async( - `standard`: The default. Balances speed and depth, a good fit for most questions. - `deep`: Spends more time researching and cross-referencing sources. Use this when accuracy and thoroughness matter more than speed. - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex research tasks where you want the highest quality result. + :param background: When true, queue a research task and return a task handle immediately instead of waiting for the result inline. Defaults to synchronous. When enabled, the response is a TaskResponse object with a task_id and stream_url for polling progress via GET /v1/research/{task_id} or streaming via GET /v1/research/{task_id}/stream. + :param source_control: Beta. Controls which web sources the research agent searches and visits. Use this to allow specific domains, block specific domains, boost specific domains, filter by recency, or focus web results by country. + + `include_domains` and `exclude_domains` cannot be used together. Each domain list is capped at 500 entries. `exclude_domains` also blocks the research agent from visiting pages on those domains during browsing. `boost_domains` gives matching domains a relative ranking boost without filtering out other domains. It can be combined with `exclude_domains` but cannot be combined with `include_domains`. + :param output_schema: Beta. Requests structured JSON output in output.content using a supported JSON Schema subset. Supported only with research_effort values standard, deep, and exhaustive. Sending output_schema with research_effort: \"lite\" returns 422. + + Schema rules: Root must be a JSON object. Top-level anyOf is not allowed. Every object must define properties and set additionalProperties: false. Every property must be listed in required. Recursive schemas are not supported. + + Limits: Max nesting depth 5, max total properties 100, max total enum values 500, max total schema string budget 25,000. :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -358,6 +731,13 @@ async def research_async( request = models.ResearchRequest( input=input, research_effort=research_effort, + background=background, + source_control=utils.get_pydantic_model( + source_control, Optional[models.SourceControl] + ), + output_schema=utils.get_pydantic_model( + output_schema, Optional[models.OutputSchema] + ), ) req = self._build_request_async( @@ -397,15 +777,17 @@ async def research_async( security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), + tags=None, + extensions=None, ), request=req, - error_status_codes=["401", "403", "422", "4XX", "500", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) response_data: Any = None if utils.match_response(http_res, "200", "application/json"): - return unmarshal_json_response(models.ResearchResponse, http_res) + return unmarshal_json_response(models.ResearchResponse1, http_res) if utils.match_response(http_res, "401", "application/json"): response_data = unmarshal_json_response( errors.ResearchUnauthorizedErrorData, http_res @@ -418,9 +800,9 @@ async def research_async( raise errors.ResearchForbiddenError(response_data, http_res) if utils.match_response(http_res, "422", "application/json"): response_data = unmarshal_json_response( - errors.UnprocessableEntityErrorData, http_res + errors.ResearchUnprocessableEntityErrorData, http_res ) - raise errors.UnprocessableEntityError(response_data, http_res) + raise errors.ResearchUnprocessableEntityError(response_data, http_res) if utils.match_response(http_res, "500", "application/json"): response_data = unmarshal_json_response( errors.ResearchInternalServerErrorData, http_res @@ -434,3 +816,733 @@ async def research_async( raise errors.YouDefaultError("API error occurred", http_res, http_res_text) raise errors.YouDefaultError("Unexpected response received", http_res) + + def get_research_task( + self, + *, + task_id: str, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.TaskDetail: + r"""Get the status of a background research task + + Poll the status of a background research task created with background=true. When the task is completed, the result is included in the response. + + :param task_id: The UUID of the research task. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.GetResearchTaskRequest( + task_id=task_id, + ) + + req = self._build_request( + method="GET", + path="/v1/research/{task_id}", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + security=self.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="getResearchTask", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(models.TaskDetail, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.GetResearchTaskUnauthorizedErrorData, http_res + ) + raise errors.GetResearchTaskUnauthorizedError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.GetResearchTaskForbiddenErrorData, http_res + ) + raise errors.GetResearchTaskForbiddenError(response_data, http_res) + if utils.match_response(http_res, "404", "application/json"): + response_data = unmarshal_json_response( + errors.GetResearchTaskNotFoundErrorData, http_res + ) + raise errors.GetResearchTaskNotFoundError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.GetResearchTaskInternalServerErrorData, http_res + ) + raise errors.GetResearchTaskInternalServerError(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + raise errors.YouDefaultError("Unexpected response received", http_res) + + async def get_research_task_async( + self, + *, + task_id: str, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.TaskDetail: + r"""Get the status of a background research task + + Poll the status of a background research task created with background=true. When the task is completed, the result is included in the response. + + :param task_id: The UUID of the research task. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.GetResearchTaskRequest( + task_id=task_id, + ) + + req = self._build_request_async( + method="GET", + path="/v1/research/{task_id}", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + security=self.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="getResearchTask", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(models.TaskDetail, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.GetResearchTaskUnauthorizedErrorData, http_res + ) + raise errors.GetResearchTaskUnauthorizedError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.GetResearchTaskForbiddenErrorData, http_res + ) + raise errors.GetResearchTaskForbiddenError(response_data, http_res) + if utils.match_response(http_res, "404", "application/json"): + response_data = unmarshal_json_response( + errors.GetResearchTaskNotFoundErrorData, http_res + ) + raise errors.GetResearchTaskNotFoundError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.GetResearchTaskInternalServerErrorData, http_res + ) + raise errors.GetResearchTaskInternalServerError(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + raise errors.YouDefaultError("Unexpected response received", http_res) + + def stream_research_task( + self, + *, + task_id: str, + from_id: Optional[int] = 0, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> eventstreaming.EventStream[models.ResearchTaskStreamEvent]: + r"""Stream updates for a background research task + + Stream real-time updates for a background research task via Server-Sent Events (SSE). Supports reconnection via the from_id query parameter to replay missed events. The connection closes automatically when the task reaches a terminal state. + + :param task_id: The UUID of the research task. + :param from_id: Resume from a sequence number for reconnection. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.StreamResearchTaskRequest( + task_id=task_id, + from_id=from_id, + ) + + req = self._build_request( + method="GET", + path="/v1/research/{task_id}/stream", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="text/event-stream", + http_headers=http_headers, + security=self.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="streamResearchTask", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + stream=True, + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "text/event-stream"): + return eventstreaming.EventStream( + http_res, + lambda raw: unmarshal_json_response( + models.ResearchTaskStreamEvent, http_res, raw + ), + client_ref=self, + ) + if utils.match_response(http_res, "401", "application/json"): + http_res_text = utils.stream_to_text(http_res) + response_data = unmarshal_json_response( + errors.StreamResearchTaskUnauthorizedErrorData, http_res, http_res_text + ) + raise errors.StreamResearchTaskUnauthorizedError( + response_data, http_res, http_res_text + ) + if utils.match_response(http_res, "403", "application/json"): + http_res_text = utils.stream_to_text(http_res) + response_data = unmarshal_json_response( + errors.StreamResearchTaskForbiddenErrorData, http_res, http_res_text + ) + raise errors.StreamResearchTaskForbiddenError( + response_data, http_res, http_res_text + ) + if utils.match_response(http_res, "404", "application/json"): + http_res_text = utils.stream_to_text(http_res) + response_data = unmarshal_json_response( + errors.StreamResearchTaskNotFoundErrorData, http_res, http_res_text + ) + raise errors.StreamResearchTaskNotFoundError( + response_data, http_res, http_res_text + ) + if utils.match_response(http_res, "500", "application/json"): + http_res_text = utils.stream_to_text(http_res) + response_data = unmarshal_json_response( + errors.StreamResearchTaskInternalServerErrorData, + http_res, + http_res_text, + ) + raise errors.StreamResearchTaskInternalServerError( + response_data, http_res, http_res_text + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError( + "Unexpected response received", http_res, http_res_text + ) + + async def stream_research_task_async( + self, + *, + task_id: str, + from_id: Optional[int] = 0, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> eventstreaming.EventStreamAsync[models.ResearchTaskStreamEvent]: + r"""Stream updates for a background research task + + Stream real-time updates for a background research task via Server-Sent Events (SSE). Supports reconnection via the from_id query parameter to replay missed events. The connection closes automatically when the task reaches a terminal state. + + :param task_id: The UUID of the research task. + :param from_id: Resume from a sequence number for reconnection. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.StreamResearchTaskRequest( + task_id=task_id, + from_id=from_id, + ) + + req = self._build_request_async( + method="GET", + path="/v1/research/{task_id}/stream", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="text/event-stream", + http_headers=http_headers, + security=self.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="streamResearchTask", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + stream=True, + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "text/event-stream"): + return eventstreaming.EventStreamAsync( + http_res, + lambda raw: unmarshal_json_response( + models.ResearchTaskStreamEvent, http_res, raw + ), + client_ref=self, + ) + if utils.match_response(http_res, "401", "application/json"): + http_res_text = await utils.stream_to_text_async(http_res) + response_data = unmarshal_json_response( + errors.StreamResearchTaskUnauthorizedErrorData, http_res, http_res_text + ) + raise errors.StreamResearchTaskUnauthorizedError( + response_data, http_res, http_res_text + ) + if utils.match_response(http_res, "403", "application/json"): + http_res_text = await utils.stream_to_text_async(http_res) + response_data = unmarshal_json_response( + errors.StreamResearchTaskForbiddenErrorData, http_res, http_res_text + ) + raise errors.StreamResearchTaskForbiddenError( + response_data, http_res, http_res_text + ) + if utils.match_response(http_res, "404", "application/json"): + http_res_text = await utils.stream_to_text_async(http_res) + response_data = unmarshal_json_response( + errors.StreamResearchTaskNotFoundErrorData, http_res, http_res_text + ) + raise errors.StreamResearchTaskNotFoundError( + response_data, http_res, http_res_text + ) + if utils.match_response(http_res, "500", "application/json"): + http_res_text = await utils.stream_to_text_async(http_res) + response_data = unmarshal_json_response( + errors.StreamResearchTaskInternalServerErrorData, + http_res, + http_res_text, + ) + raise errors.StreamResearchTaskInternalServerError( + response_data, http_res, http_res_text + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError( + "Unexpected response received", http_res, http_res_text + ) + + def finance_research( + self, + *, + input: str, + research_effort: Optional[ + models.FinanceResearchEffort + ] = models.FinanceResearchEffort.DEEP, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.FinanceResearchResponse: + r"""Returns comprehensive finance-grade research answers with multi-step reasoning + + The Finance Research API is purpose-built for financial questions. Like the Research API, it runs multiple searches, reads through sources, and synthesizes everything into a thorough, well-cited answer — but its retrieval index is optimized for financial data: earnings reports, SEC filings, analyst coverage, market data, and financial news. + Use it when you need credible, sourced answers to financial questions: company fundamentals, market trends, competitive analysis, earnings summaries, or macroeconomic research. + + :param input: The financial research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + :param research_effort: Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.FinanceResearchRequest( + input=input, + research_effort=research_effort, + ) + + req = self._build_request( + method="POST", + path="/v1/finance_research", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=True, + request_has_path_params=False, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + security=self.sdk_configuration.security, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, False, "json", models.FinanceResearchRequest + ), + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="finance_research", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(models.FinanceResearchResponse, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.FinanceResearchUnauthorizedErrorData, http_res + ) + raise errors.FinanceResearchUnauthorizedError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.FinanceResearchForbiddenErrorData, http_res + ) + raise errors.FinanceResearchForbiddenError(response_data, http_res) + if utils.match_response(http_res, "422", "application/json"): + response_data = unmarshal_json_response( + errors.FinanceResearchUnprocessableEntityErrorData, http_res + ) + raise errors.FinanceResearchUnprocessableEntityError( + response_data, http_res + ) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.FinanceResearchInternalServerErrorData, http_res + ) + raise errors.FinanceResearchInternalServerError(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + raise errors.YouDefaultError("Unexpected response received", http_res) + + async def finance_research_async( + self, + *, + input: str, + research_effort: Optional[ + models.FinanceResearchEffort + ] = models.FinanceResearchEffort.DEEP, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.FinanceResearchResponse: + r"""Returns comprehensive finance-grade research answers with multi-step reasoning + + The Finance Research API is purpose-built for financial questions. Like the Research API, it runs multiple searches, reads through sources, and synthesizes everything into a thorough, well-cited answer — but its retrieval index is optimized for financial data: earnings reports, SEC filings, analyst coverage, market data, and financial news. + Use it when you need credible, sourced answers to financial questions: company fundamentals, market trends, competitive analysis, earnings summaries, or macroeconomic research. + + :param input: The financial research question or complex query requiring in-depth investigation and multi-step reasoning. + + Note: The maximum length of the input is 40,000 characters. + :param research_effort: Controls how much time and effort the Finance Research API spends on your question. Higher effort levels run more searches and dig deeper into sources, at the cost of a longer response time. + + Available levels: + - `deep`: The default. Spends more time researching and cross-referencing sources. Good for most financial questions, including multi-company comparisons, earnings analysis, and regulatory research. + - `exhaustive`: The most thorough option. Explores the topic as fully as possible, best suited for complex financial research tasks where you want the highest quality result. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.FinanceResearchRequest( + input=input, + research_effort=research_effort, + ) + + req = self._build_request_async( + method="POST", + path="/v1/finance_research", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=True, + request_has_path_params=False, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + security=self.sdk_configuration.security, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, False, "json", models.FinanceResearchRequest + ), + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["429", "500", "502", "503", "504"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="finance_research", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=None, + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) + + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(models.FinanceResearchResponse, http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.FinanceResearchUnauthorizedErrorData, http_res + ) + raise errors.FinanceResearchUnauthorizedError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.FinanceResearchForbiddenErrorData, http_res + ) + raise errors.FinanceResearchForbiddenError(response_data, http_res) + if utils.match_response(http_res, "422", "application/json"): + response_data = unmarshal_json_response( + errors.FinanceResearchUnprocessableEntityErrorData, http_res + ) + raise errors.FinanceResearchUnprocessableEntityError( + response_data, http_res + ) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.FinanceResearchInternalServerErrorData, http_res + ) + raise errors.FinanceResearchInternalServerError(response_data, http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.YouDefaultError("API error occurred", http_res, http_res_text) + + raise errors.YouDefaultError("Unexpected response received", http_res) diff --git a/src/youdotcom/search.py b/src/youdotcom/search.py index 355ce77..9e69fa0 100644 --- a/src/youdotcom/search.py +++ b/src/youdotcom/search.py @@ -1,7 +1,7 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" from .basesdk import BaseSDK -from typing import Any, Mapping, Optional, Union +from typing import Any, Iterable, List, Mapping, Optional, Union from youdotcom import errors, models, utils from youdotcom._hooks import HookContext from youdotcom.types import OptionalNullable, UNSET @@ -13,25 +13,21 @@ class Search(BaseSDK): def unified( self, *, - query: str = "Your query", + query: str, count: Optional[int] = 10, freshness: Optional[ - Union[models.SearchFreshness, models.SearchFreshnessTypedDict] + Union[models.FreshnessValue, models.FreshnessValueTypedDict] ] = None, offset: Optional[int] = None, - country: Optional[ - Union[models.SearchCountry, models.SearchCountryTypedDict] - ] = None, + country: Optional[models.Country] = None, language: Optional[models.Language] = models.Language.EN, - safesearch: Optional[ - Union[models.SearchSafesearch, models.SearchSafesearchTypedDict] - ] = None, - livecrawl: Optional[ - Union[models.SearchLivecrawl, models.SearchLivecrawlTypedDict] - ] = None, - livecrawl_formats: Optional[ - Union[models.SearchLivecrawlFormats, models.SearchLivecrawlFormatsTypedDict] - ] = None, + safesearch: Optional[models.SafeSearch] = None, + livecrawl: Optional[models.LiveCrawl] = None, + livecrawl_formats: Optional[Iterable[models.LiveCrawlFormats]] = None, + include_domains: Optional[str] = None, + exclude_domains: Optional[str] = None, + boost_domains: Optional[str] = None, + crawl_timeout: Optional[int] = 10, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, @@ -41,17 +37,29 @@ def unified( This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. - :param query: The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. - :param count: Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). + `GET` is a good choice for simple queries where HTTP cacheability matters—GET responses can be cached at CDN and proxy layers, whereas POST responses are not cached by default per the HTTP spec. For requests with complex parameters such as `include_domains` or `exclude_domains`, use POST instead - domain lists are passed as comma-separated strings in GET and are limited by URL length. + + :param query: + :param count: :param freshness: Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. - :param offset: Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. + :param offset: :param country: The country code that determines the geographical focus of the web results. :param language: The language of the web results that will be returned (BCP 47 format). :param safesearch: Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. :param livecrawl: Indicates which section(s) of search results to livecrawl and return full page content. - :param livecrawl_formats: Indicates the format of the livecrawled content. + :param livecrawl_formats: + :param include_domains: A list of domains to restrict search results to. Only results from these domains will be returned. For large domain lists (up to 500), use POST with a JSON array instead. This is a strict allowlist — cannot be combined with `exclude_domains` (returns `422`). + + **Important:** Use a single comma-separated value (e.g. `include_domains=nytimes.com,bbc.com`). Repeated parameters (`include_domains=a.com&include_domains=b.com`) are not supported. + :param exclude_domains: A list of domains to exclude from search results. Results from these domains will be filtered out. For large domain lists (up to 500), use POST with a JSON array instead. Cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `exclude_domains=spam-site.com,other-site.com`). Repeated parameters are not supported. + :param boost_domains: A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `boost_domains=nytimes.com,wired.com`). Repeated parameters are not supported. + :param crawl_timeout: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -76,7 +84,13 @@ def unified( language=language, safesearch=safesearch, livecrawl=livecrawl, - livecrawl_formats=livecrawl_formats, + livecrawl_formats=utils.unmarshal( + livecrawl_formats, Optional[List[models.LiveCrawlFormats]] + ), + include_domains=include_domains, + exclude_domains=exclude_domains, + boost_domains=boost_domains, + crawl_timeout=crawl_timeout, ) req = self._build_request( @@ -113,9 +127,11 @@ def unified( security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), + tags=["search"], + extensions=None, ), request=req, - error_status_codes=["401", "403", "4XX", "500", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -124,19 +140,24 @@ def unified( return unmarshal_json_response(models.SearchResponse, http_res) if utils.match_response(http_res, "401", "application/json"): response_data = unmarshal_json_response( - errors.SearchUnauthorizedErrorData, http_res + errors.UnauthorizedResponseErrorData, http_res ) - raise errors.SearchUnauthorizedError(response_data, http_res) + raise errors.UnauthorizedResponseError(response_data, http_res) if utils.match_response(http_res, "403", "application/json"): response_data = unmarshal_json_response( - errors.SearchForbiddenErrorData, http_res + errors.ForbiddenResponseErrorData, http_res + ) + raise errors.ForbiddenResponseError(response_data, http_res) + if utils.match_response(http_res, "422", "application/json"): + response_data = unmarshal_json_response( + errors.UnprocessableEntityResponseErrorData, http_res ) - raise errors.SearchForbiddenError(response_data, http_res) + raise errors.UnprocessableEntityResponseError(response_data, http_res) if utils.match_response(http_res, "500", "application/json"): response_data = unmarshal_json_response( - errors.SearchInternalServerErrorData, http_res + errors.InternalServerErrorResponseData, http_res ) - raise errors.SearchInternalServerError(response_data, http_res) + raise errors.InternalServerErrorResponse(response_data, http_res) if utils.match_response(http_res, "4XX", "*"): http_res_text = utils.stream_to_text(http_res) raise errors.YouDefaultError("API error occurred", http_res, http_res_text) @@ -149,25 +170,21 @@ def unified( async def unified_async( self, *, - query: str = "Your query", + query: str, count: Optional[int] = 10, freshness: Optional[ - Union[models.SearchFreshness, models.SearchFreshnessTypedDict] + Union[models.FreshnessValue, models.FreshnessValueTypedDict] ] = None, offset: Optional[int] = None, - country: Optional[ - Union[models.SearchCountry, models.SearchCountryTypedDict] - ] = None, + country: Optional[models.Country] = None, language: Optional[models.Language] = models.Language.EN, - safesearch: Optional[ - Union[models.SearchSafesearch, models.SearchSafesearchTypedDict] - ] = None, - livecrawl: Optional[ - Union[models.SearchLivecrawl, models.SearchLivecrawlTypedDict] - ] = None, - livecrawl_formats: Optional[ - Union[models.SearchLivecrawlFormats, models.SearchLivecrawlFormatsTypedDict] - ] = None, + safesearch: Optional[models.SafeSearch] = None, + livecrawl: Optional[models.LiveCrawl] = None, + livecrawl_formats: Optional[Iterable[models.LiveCrawlFormats]] = None, + include_domains: Optional[str] = None, + exclude_domains: Optional[str] = None, + boost_domains: Optional[str] = None, + crawl_timeout: Optional[int] = 10, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, @@ -177,17 +194,29 @@ async def unified_async( This endpoint is designed to return LLM-ready web results based on a user's query. Based on a classification mechanism, it can return web results and news associated with your query. If you need to feed an LLM with the results of a query that sounds like `What are the latest geopolitical updates from India`, then this endpoint is the right one for you. - :param query: The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. - :param count: Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). + `GET` is a good choice for simple queries where HTTP cacheability matters—GET responses can be cached at CDN and proxy layers, whereas POST responses are not cached by default per the HTTP spec. For requests with complex parameters such as `include_domains` or `exclude_domains`, use POST instead - domain lists are passed as comma-separated strings in GET and are limited by URL length. + + :param query: + :param count: :param freshness: Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`. When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. - :param offset: Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. + :param offset: :param country: The country code that determines the geographical focus of the web results. :param language: The language of the web results that will be returned (BCP 47 format). :param safesearch: Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. :param livecrawl: Indicates which section(s) of search results to livecrawl and return full page content. - :param livecrawl_formats: Indicates the format of the livecrawled content. + :param livecrawl_formats: + :param include_domains: A list of domains to restrict search results to. Only results from these domains will be returned. For large domain lists (up to 500), use POST with a JSON array instead. This is a strict allowlist — cannot be combined with `exclude_domains` (returns `422`). + + **Important:** Use a single comma-separated value (e.g. `include_domains=nytimes.com,bbc.com`). Repeated parameters (`include_domains=a.com&include_domains=b.com`) are not supported. + :param exclude_domains: A list of domains to exclude from search results. Results from these domains will be filtered out. For large domain lists (up to 500), use POST with a JSON array instead. Cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `exclude_domains=spam-site.com,other-site.com`). Repeated parameters are not supported. + :param boost_domains: A list of domains to boost in search ranking. Matching results from these domains receive a relative ranking boost, but results are not limited to these domains. Supports up to 500 domains. Can be combined with `exclude_domains`, but cannot be combined with `include_domains` (returns `422`). + + **Important:** You must use a single comma-separated value (e.g. `boost_domains=nytimes.com,wired.com`). Repeated parameters are not supported. + :param crawl_timeout: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds @@ -212,7 +241,13 @@ async def unified_async( language=language, safesearch=safesearch, livecrawl=livecrawl, - livecrawl_formats=livecrawl_formats, + livecrawl_formats=utils.unmarshal( + livecrawl_formats, Optional[List[models.LiveCrawlFormats]] + ), + include_domains=include_domains, + exclude_domains=exclude_domains, + boost_domains=boost_domains, + crawl_timeout=crawl_timeout, ) req = self._build_request_async( @@ -249,9 +284,11 @@ async def unified_async( security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), + tags=["search"], + extensions=None, ), request=req, - error_status_codes=["401", "403", "4XX", "500", "5XX"], + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), retry_config=retry_config, ) @@ -260,19 +297,24 @@ async def unified_async( return unmarshal_json_response(models.SearchResponse, http_res) if utils.match_response(http_res, "401", "application/json"): response_data = unmarshal_json_response( - errors.SearchUnauthorizedErrorData, http_res + errors.UnauthorizedResponseErrorData, http_res ) - raise errors.SearchUnauthorizedError(response_data, http_res) + raise errors.UnauthorizedResponseError(response_data, http_res) if utils.match_response(http_res, "403", "application/json"): response_data = unmarshal_json_response( - errors.SearchForbiddenErrorData, http_res + errors.ForbiddenResponseErrorData, http_res + ) + raise errors.ForbiddenResponseError(response_data, http_res) + if utils.match_response(http_res, "422", "application/json"): + response_data = unmarshal_json_response( + errors.UnprocessableEntityResponseErrorData, http_res ) - raise errors.SearchForbiddenError(response_data, http_res) + raise errors.UnprocessableEntityResponseError(response_data, http_res) if utils.match_response(http_res, "500", "application/json"): response_data = unmarshal_json_response( - errors.SearchInternalServerErrorData, http_res + errors.InternalServerErrorResponseData, http_res ) - raise errors.SearchInternalServerError(response_data, http_res) + raise errors.InternalServerErrorResponse(response_data, http_res) if utils.match_response(http_res, "4XX", "*"): http_res_text = await utils.stream_to_text_async(http_res) raise errors.YouDefaultError("API error occurred", http_res, http_res_text) diff --git a/src/youdotcom/types/__init__.py b/src/youdotcom/types/__init__.py index fc76fe0..faa2681 100644 --- a/src/youdotcom/types/__init__.py +++ b/src/youdotcom/types/__init__.py @@ -1,5 +1,6 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +from .base64fileinput import Base64EncodedString, Base64FileInput from .basemodel import ( BaseModel, Nullable, @@ -11,6 +12,8 @@ ) __all__ = [ + "Base64EncodedString", + "Base64FileInput", "BaseModel", "Nullable", "OptionalNullable", diff --git a/src/youdotcom/types/base64fileinput.py b/src/youdotcom/types/base64fileinput.py new file mode 100644 index 0000000..862566f --- /dev/null +++ b/src/youdotcom/types/base64fileinput.py @@ -0,0 +1,43 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations + +import base64 +import io +from os import PathLike +from typing import IO, Any, Union + +from pydantic.functional_validators import BeforeValidator +from typing_extensions import Annotated + + +Base64FileInput = Union[IO[bytes], PathLike[str]] + + +def encode_base64_file_input(value: Any) -> Any: + """Convert PathLike or IO[bytes] inputs to a base64 string. All standard binary streams + that inherit from io.IOBase are handled. Other values pass through. + """ + if isinstance(value, (PathLike, io.IOBase)): + if isinstance(value, PathLike): + with open(value, "rb") as fh: + binary = fh.read() + else: + # Restore position after reading: pydantic may validate the same stream more than once. + position = value.tell() if value.seekable() else None + binary = value.read() + if position is not None: + value.seek(position) + if isinstance(binary, str): + binary = binary.encode() + if not isinstance(binary, (bytes, bytearray)): + raise TypeError( + f"Base64FileInput expected binary IO returning bytes; got {type(binary).__name__}" + ) + return base64.b64encode(binary).decode("ascii") + return value + + +# Non-str inputs are converted to base64 by the BeforeValidator at construction time. +# Callers can also pass a pre-encoded base64 str. +Base64EncodedString = Annotated[str, BeforeValidator(encode_base64_file_input)] diff --git a/src/youdotcom/utils/__init__.py b/src/youdotcom/utils/__init__.py index aded759..c48a36c 100644 --- a/src/youdotcom/utils/__init__.py +++ b/src/youdotcom/utils/__init__.py @@ -15,7 +15,7 @@ async def run_sync_in_thread(func: Callable[..., _T], *args) -> _T: if TYPE_CHECKING: from .annotations import get_discriminator - from .datetimes import parse_datetime + from .datetimes import parse_datetime, parse_duration from .enums import OpenEnumMeta from .headers import get_headers, get_response_headers from .metadata import ( @@ -71,6 +71,7 @@ async def run_sync_in_thread(func: Callable[..., _T], *args) -> _T: "get_default_logger", "get_discriminator", "parse_datetime", + "parse_duration", "get_global_from_env", "get_headers", "get_pydantic_model", @@ -124,6 +125,7 @@ async def run_sync_in_thread(func: Callable[..., _T], *args) -> _T: "get_default_logger": ".logger", "get_discriminator": ".annotations", "parse_datetime": ".datetimes", + "parse_duration": ".datetimes", "get_global_from_env": ".values", "get_headers": ".headers", "get_pydantic_model": ".serializers", diff --git a/src/youdotcom/utils/datetimes.py b/src/youdotcom/utils/datetimes.py index a6c52cd..adad247 100644 --- a/src/youdotcom/utils/datetimes.py +++ b/src/youdotcom/utils/datetimes.py @@ -1,8 +1,10 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" -from datetime import datetime +from datetime import datetime, timedelta import sys +from pydantic import TypeAdapter + def parse_datetime(datetime_string: str) -> datetime: """ @@ -21,3 +23,15 @@ def parse_datetime(datetime_string: str) -> datetime: datetime_string = datetime_string[:-1] + "+00:00" return datetime.fromisoformat(datetime_string) + + +_DURATION_ADAPTER = TypeAdapter(timedelta) + + +def parse_duration(duration_string: str) -> timedelta: + """ + Convert an ISO 8601 duration string (e.g. "PT1H30M") into a timedelta. + The standard library has no ISO 8601 duration parser, so this delegates to + pydantic, which is already a runtime dependency of the SDK. + """ + return _DURATION_ADAPTER.validate_python(duration_string) diff --git a/src/youdotcom/utils/eventstreaming.py b/src/youdotcom/utils/eventstreaming.py index f2052fc..a8d4fe5 100644 --- a/src/youdotcom/utils/eventstreaming.py +++ b/src/youdotcom/utils/eventstreaming.py @@ -7,6 +7,7 @@ Any, Callable, Generic, + List, TypeVar, Optional, Generator, @@ -32,9 +33,12 @@ def __init__( decoder: Callable[[str], T], sentinel: Optional[str] = None, client_ref: Optional[object] = None, + data_required: bool = True, ): self.response = response - self.generator = stream_events(response, decoder, sentinel) + self.generator = stream_events( + response, decoder, sentinel, data_required=data_required + ) self.client_ref = client_ref self._closed = False @@ -50,6 +54,9 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self): self._closed = True self.response.close() @@ -68,9 +75,12 @@ def __init__( decoder: Callable[[str], T], sentinel: Optional[str] = None, client_ref: Optional[object] = None, + data_required: bool = True, ): self.response = response - self.generator = stream_events_async(response, decoder, sentinel) + self.generator = stream_events_async( + response, decoder, sentinel, data_required=data_required + ) self.client_ref = client_ref self._closed = False @@ -86,6 +96,9 @@ async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + + async def close(self): self._closed = True await self.response.aclose() @@ -108,6 +121,7 @@ class ServerEvent: b"\n\r", b"\n\n", ] +MAX_BOUNDARY_LEN = max(len(b) for b in MESSAGE_BOUNDARIES) UTF8_BOM = b"\xef\xbb\xbf" @@ -116,90 +130,116 @@ async def stream_events_async( response: httpx.Response, decoder: Callable[[str], T], sentinel: Optional[str] = None, + data_required: bool = True, ) -> AsyncGenerator[T, None]: - buffer = bytearray() - position = 0 - event_id: Optional[str] = None - async for chunk in response.aiter_bytes(): - if len(buffer) == 0 and chunk.startswith(UTF8_BOM): - chunk = chunk[len(UTF8_BOM) :] - buffer += chunk - for i in range(position, len(buffer)): - char = buffer[i : i + 1] - seq: Optional[bytes] = None - if char in [b"\r", b"\n"]: - for boundary in MESSAGE_BOUNDARIES: - seq = _peek_sequence(i, buffer, boundary) - if seq is not None: - break - if seq is None: - continue - - block = buffer[position:i] - position = i + len(seq) - event, discard, event_id = _parse_event( - raw=block, decoder=decoder, sentinel=sentinel, event_id=event_id - ) - if event is not None: - yield event - if discard: - await response.aclose() - return - - if position > 0: - buffer = buffer[position:] - position = 0 - - event, discard, _ = _parse_event( - raw=buffer, decoder=decoder, sentinel=sentinel, event_id=event_id - ) - if event is not None: - yield event + try: + buffer = bytearray() + position = 0 + event_id: Optional[str] = None + async for chunk in response.aiter_bytes(): + if len(buffer) == 0 and chunk.startswith(UTF8_BOM): + chunk = chunk[len(UTF8_BOM) :] + old_len = len(buffer) + buffer += chunk + search_start = max(position, old_len - MAX_BOUNDARY_LEN + 1) + for i in range(search_start, len(buffer)): + char = buffer[i : i + 1] + seq: Optional[bytes] = None + if char in [b"\r", b"\n"]: + for boundary in MESSAGE_BOUNDARIES: + seq = _peek_sequence(i, buffer, boundary) + if seq is not None: + break + if seq is None: + continue + + block = buffer[position:i] + position = i + len(seq) + event, discard, event_id = _parse_event( + raw=block, + decoder=decoder, + sentinel=sentinel, + event_id=event_id, + data_required=data_required, + ) + if event is not None: + yield event + if discard: + return + + if position > 0: + buffer = buffer[position:] + position = 0 + + event, discard, _ = _parse_event( + raw=buffer, + decoder=decoder, + sentinel=sentinel, + event_id=event_id, + data_required=data_required, + ) + if event is not None: + yield event + finally: + await response.aclose() def stream_events( response: httpx.Response, decoder: Callable[[str], T], sentinel: Optional[str] = None, + data_required: bool = True, ) -> Generator[T, None, None]: - buffer = bytearray() - position = 0 - event_id: Optional[str] = None - for chunk in response.iter_bytes(): - if len(buffer) == 0 and chunk.startswith(UTF8_BOM): - chunk = chunk[len(UTF8_BOM) :] - buffer += chunk - for i in range(position, len(buffer)): - char = buffer[i : i + 1] - seq: Optional[bytes] = None - if char in [b"\r", b"\n"]: - for boundary in MESSAGE_BOUNDARIES: - seq = _peek_sequence(i, buffer, boundary) - if seq is not None: - break - if seq is None: - continue - - block = buffer[position:i] - position = i + len(seq) - event, discard, event_id = _parse_event( - raw=block, decoder=decoder, sentinel=sentinel, event_id=event_id - ) - if event is not None: - yield event - if discard: - response.close() - return - - if position > 0: - buffer = buffer[position:] - position = 0 - - event, discard, _ = _parse_event( - raw=buffer, decoder=decoder, sentinel=sentinel, event_id=event_id - ) - if event is not None: - yield event + try: + buffer = bytearray() + position = 0 + event_id: Optional[str] = None + for chunk in response.iter_bytes(): + if len(buffer) == 0 and chunk.startswith(UTF8_BOM): + chunk = chunk[len(UTF8_BOM) :] + old_len = len(buffer) + buffer += chunk + search_start = max(position, old_len - MAX_BOUNDARY_LEN + 1) + for i in range(search_start, len(buffer)): + char = buffer[i : i + 1] + seq: Optional[bytes] = None + if char in [b"\r", b"\n"]: + for boundary in MESSAGE_BOUNDARIES: + seq = _peek_sequence(i, buffer, boundary) + if seq is not None: + break + if seq is None: + continue + + block = buffer[position:i] + position = i + len(seq) + event, discard, event_id = _parse_event( + raw=block, + decoder=decoder, + sentinel=sentinel, + event_id=event_id, + data_required=data_required, + ) + if event is not None: + yield event + if discard: + return + + if position > 0: + buffer = buffer[position:] + position = 0 + + event, discard, _ = _parse_event( + raw=buffer, + decoder=decoder, + sentinel=sentinel, + event_id=event_id, + data_required=data_required, + ) + if event is not None: + yield event + finally: + response.close() def _parse_event( @@ -208,12 +248,13 @@ def _parse_event( decoder: Callable[[str], T], sentinel: Optional[str] = None, event_id: Optional[str] = None, + data_required: bool = True, ) -> Tuple[Optional[T], bool, Optional[str]]: block = raw.decode() lines = re.split(r"\r?\n|\r", block) publish = False event = ServerEvent() - data = "" + data_parts: List[str] = [] for line in lines: if not line: continue @@ -234,7 +275,7 @@ def _parse_event( event.event = value publish = True elif field == "data": - data += value + "\n" + data_parts.append(value) publish = True elif field == "id": publish = True @@ -246,12 +287,17 @@ def _parse_event( publish = True event.id = event_id + has_data = bool(data_parts) + data = "\n".join(data_parts) - if sentinel and data == f"{sentinel}\n": + if sentinel and has_data and data == sentinel: return None, True, event_id - if data: - data = data[:-1] + # Skip data-less events when data is required + if not has_data and publish and data_required: + return None, False, event_id + + if has_data: try: event.data = json.loads(data) except json.JSONDecodeError: @@ -262,7 +308,7 @@ def _parse_event( out_dict = { k: v for k, v in asdict(event).items() - if v is not None or (k == "data" and data) + if v is not None or (k == "data" and has_data) } out = decoder(json.dumps(out_dict)) diff --git a/src/youdotcom/utils/forms.py b/src/youdotcom/utils/forms.py index 1e550bd..193f264 100644 --- a/src/youdotcom/utils/forms.py +++ b/src/youdotcom/utils/forms.py @@ -1,5 +1,6 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +import io from typing import ( Any, Dict, @@ -103,6 +104,10 @@ def _extract_file_properties(file_obj: Any) -> Tuple[str, Any, Any]: if file_metadata.content: content = getattr(file_obj, file_field_name, None) + if isinstance(content, io.TextIOBase): + content = content.read().encode( + getattr(content, "encoding", None) or "utf-8" + ) elif file_field_name == "content_type": content_type = getattr(file_obj, file_field_name, None) else: diff --git a/src/youdotcom/utils/metadata.py b/src/youdotcom/utils/metadata.py index 173b3e5..5abddd5 100644 --- a/src/youdotcom/utils/metadata.py +++ b/src/youdotcom/utils/metadata.py @@ -15,6 +15,7 @@ class SecurityMetadata: scheme_type: Optional[str] = None sub_type: Optional[str] = None field_name: Optional[str] = None + composite: bool = False def get_field_name(self, default: str) -> str: return self.field_name or default diff --git a/src/youdotcom/utils/requestbodies.py b/src/youdotcom/utils/requestbodies.py index 1de32b6..591415a 100644 --- a/src/youdotcom/utils/requestbodies.py +++ b/src/youdotcom/utils/requestbodies.py @@ -46,6 +46,7 @@ def serialize_request_body( if re.match(r"^(application|text)\/([^+]+\+)*json.*", media_type) is not None: serialized_request_body.content = marshal_json(request_body, request_body_type) + elif re.match(r"^multipart\/.*", media_type) is not None: ( serialized_request_body.media_type, diff --git a/src/youdotcom/utils/retries.py b/src/youdotcom/utils/retries.py index af07d4e..ca7b59e 100644 --- a/src/youdotcom/utils/retries.py +++ b/src/youdotcom/utils/retries.py @@ -11,10 +11,13 @@ class BackoffStrategy: + """Exponential backoff strategy configuration.""" + initial_interval: int max_interval: int exponent: float max_elapsed_time: int + jitter_ms: Optional[int] def __init__( self, @@ -22,24 +25,63 @@ def __init__( max_interval: int, exponent: float, max_elapsed_time: int, + jitter_ms: Optional[int] = None, ): + """Initialize a backoff strategy. + + Args: + initial_interval: Initial retry interval in milliseconds. + max_interval: Maximum retry interval in milliseconds. + exponent: Base of the exponential backoff; the interval grows as + ``initial_interval * exponent ** retries``. + max_elapsed_time: Maximum total elapsed time in milliseconds. + jitter_ms: Additive jitter bound in milliseconds. When set, adds a random + value in ``[0, jitter_ms]`` to each computed backoff interval (default + ``+[0, 1s]``). + + Note: + When a response carries a ``Retry-After`` or ``retry-after-ms`` header, + that delay is used as-is and the sleep-shaping parameters + (``initial_interval``, ``max_interval``, ``exponent``, ``jitter_ms``) are + ignored for that attempt. + """ + if jitter_ms is not None and jitter_ms < 0: + raise ValueError("jitter_ms must be >= 0") self.initial_interval = initial_interval self.max_interval = max_interval self.exponent = exponent self.max_elapsed_time = max_elapsed_time + self.jitter_ms = jitter_ms class RetryConfig: + """Runtime retry configuration.""" + strategy: str backoff: BackoffStrategy retry_connection_errors: bool + status_codes_override: Optional[List[str]] def __init__( - self, strategy: str, backoff: BackoffStrategy, retry_connection_errors: bool + self, + strategy: str, + backoff: BackoffStrategy, + retry_connection_errors: bool, + status_codes_override: Optional[List[str]] = None, ): + """Initialize a retry configuration. + + Args: + strategy: Retry strategy: ``"none"`` or ``"backoff"``. + backoff: Backoff parameters. + retry_connection_errors: Whether to also retry transport-level connection errors. + status_codes_override: Retryable HTTP status codes that take precedence over the + per-operation defaults when non-empty. + """ self.strategy = strategy self.backoff = backoff self.retry_connection_errors = retry_connection_errors + self.status_codes_override = status_codes_override class Retries: @@ -48,7 +90,7 @@ class Retries: def __init__(self, config: RetryConfig, status_codes: List[str]): self.config = config - self.status_codes = status_codes + self.status_codes = config.status_codes_override or status_codes class TemporaryError(Exception): @@ -93,12 +135,28 @@ def _parse_retry_after_header(response: httpx.Response) -> Optional[int]: return None +def _parse_retry_after_ms_header(response: httpx.Response) -> Optional[int]: + retry_after_ms_header = response.headers.get("retry-after-ms") + if not retry_after_ms_header: + return None + + try: + milliseconds = float(retry_after_ms_header) + if milliseconds >= 0: + return round(milliseconds) + except (OverflowError, ValueError): + pass + + return None + + def _get_sleep_interval( exception: Exception, initial_interval: int, max_interval: int, exponent: float, retries: int, + jitter_ms: Optional[int] = None, ) -> float: """Get sleep interval for retry with exponential backoff. @@ -108,6 +166,7 @@ def _get_sleep_interval( max_interval: Maximum retry interval in milliseconds. exponent: Base for exponential backoff calculation. retries: Current retry attempt count. + jitter_ms: Additive jitter bound in ms; see ``BackoffStrategy.jitter_ms``. Returns: Sleep interval in seconds. @@ -119,7 +178,11 @@ def _get_sleep_interval( ): return exception.retry_after / 1000 - sleep = (initial_interval / 1000) * exponent**retries + random.uniform(0, 1) + sleep = (initial_interval / 1000) * exponent**retries + if jitter_ms is not None: + sleep += random.uniform(0, jitter_ms / 1000) + else: + sleep += random.uniform(0, 1) return min(sleep, max_interval / 1000) @@ -162,6 +225,7 @@ def do_request() -> httpx.Response: retries.config.backoff.max_interval, retries.config.backoff.exponent, retries.config.backoff.max_elapsed_time, + retries.config.backoff.jitter_ms, ) return func() @@ -206,6 +270,7 @@ async def do_request() -> httpx.Response: retries.config.backoff.max_interval, retries.config.backoff.exponent, retries.config.backoff.max_elapsed_time, + retries.config.backoff.jitter_ms, ) return await func() @@ -217,6 +282,7 @@ def retry_with_backoff( max_interval=60000, exponent=1.5, max_elapsed_time=3600000, + jitter_ms=None, ): start = round(time.time() * 1000) retries = 0 @@ -234,8 +300,17 @@ def retry_with_backoff( raise + if isinstance(exception, TemporaryError): + retry_after_ms = _parse_retry_after_ms_header(exception.response) + if retry_after_ms is not None: + exception.retry_after = retry_after_ms sleep = _get_sleep_interval( - exception, initial_interval, max_interval, exponent, retries + exception, + initial_interval, + max_interval, + exponent, + retries, + jitter_ms=jitter_ms, ) time.sleep(sleep) retries += 1 @@ -247,6 +322,7 @@ async def retry_with_backoff_async( max_interval=60000, exponent=1.5, max_elapsed_time=3600000, + jitter_ms=None, ): start = round(time.time() * 1000) retries = 0 @@ -264,8 +340,17 @@ async def retry_with_backoff_async( raise + if isinstance(exception, TemporaryError): + retry_after_ms = _parse_retry_after_ms_header(exception.response) + if retry_after_ms is not None: + exception.retry_after = retry_after_ms sleep = _get_sleep_interval( - exception, initial_interval, max_interval, exponent, retries + exception, + initial_interval, + max_interval, + exponent, + retries, + jitter_ms=jitter_ms, ) await asyncio.sleep(sleep) retries += 1 diff --git a/src/youdotcom/utils/security.py b/src/youdotcom/utils/security.py index e51915d..5c4b37b 100644 --- a/src/youdotcom/utils/security.py +++ b/src/youdotcom/utils/security.py @@ -19,7 +19,9 @@ import os -def get_security(security: Any) -> Tuple[Dict[str, str], Dict[str, List[str]]]: +def get_security( + security: Any, allowed_fields: Optional[List[str]] = None +) -> Tuple[Dict[str, str], Dict[str, List[str]]]: headers: Dict[str, str] = {} query_params: Dict[str, List[str]] = {} @@ -30,7 +32,14 @@ def get_security(security: Any) -> Tuple[Dict[str, str], Dict[str, List[str]]]: raise TypeError("security must be a pydantic model") sec_fields: Dict[str, FieldInfo] = security.__class__.model_fields - for name in sec_fields: + sec_field_names = ( + list(sec_fields.keys()) if allowed_fields is None else allowed_fields + ) + + for name in sec_field_names: + if name not in sec_fields: + continue + sec_field = sec_fields[name] value = getattr(security, name) @@ -52,6 +61,9 @@ def get_security(security: Any) -> Tuple[Dict[str, str], Dict[str, List[str]]]: else: _parse_security_scheme(headers, query_params, metadata, name, value) + if not metadata.composite: + return headers, query_params + return headers, query_params @@ -64,8 +76,9 @@ def get_security_from_env(security: Any, security_class: Any) -> Optional[BaseMo security_dict: Any = {} - if os.getenv("YOU_API_KEY_AUTH"): - security_dict["api_key_auth"] = os.getenv("YOU_API_KEY_AUTH") + api_key = os.getenv("YDC_API_KEY") or os.getenv("YOU_API_KEY_AUTH") + if api_key: + security_dict["api_key_auth"] = api_key return security_class(**security_dict) if security_dict else None @@ -77,15 +90,24 @@ def _parse_security_option( raise TypeError("security option must be a pydantic model") opt_fields: Dict[str, FieldInfo] = option.__class__.model_fields + for name in opt_fields: opt_field = opt_fields[name] metadata = find_field_metadata(opt_field, SecurityMetadata) if metadata is None or not metadata.scheme: continue - _parse_security_scheme( - headers, query_params, metadata, name, getattr(option, name) - ) + + value = getattr(option, name) + if ( + metadata.scheme_type == "http" + and metadata.sub_type == "basic" + and not isinstance(value, BaseModel) + ): + _parse_basic_auth_scheme(headers, option) + return + + _parse_security_scheme(headers, query_params, metadata, name, value) def _parse_security_scheme( diff --git a/src/youdotcom/utils/serializers.py b/src/youdotcom/utils/serializers.py index 14321eb..1031ed9 100644 --- a/src/youdotcom/utils/serializers.py +++ b/src/youdotcom/utils/serializers.py @@ -4,7 +4,7 @@ import functools import json import typing -from typing import Any, Dict, List, Tuple, Union, get_args +from typing import Any, Dict, Iterable, List, Mapping, Tuple, Union, get_args import typing_extensions from typing_extensions import get_origin @@ -17,8 +17,7 @@ def serialize_decimal(as_str: bool): def serialize(d): - # Optional[T] is a Union[T, None] - if is_union(type(d)) and type(None) in get_args(type(d)) and d is None: + if d is None: return None if isinstance(d, Unset): return d @@ -46,8 +45,7 @@ def validate_decimal(d): def serialize_float(as_str: bool): def serialize(f): - # Optional[T] is a Union[T, None] - if is_union(type(f)) and type(None) in get_args(type(f)) and f is None: + if f is None: return None if isinstance(f, Unset): return f @@ -75,8 +73,7 @@ def validate_float(f): def serialize_int(as_str: bool): def serialize(i): - # Optional[T] is a Union[T, None] - if is_union(type(i)) and type(None) in get_args(type(i)) and i is None: + if i is None: return None if isinstance(i, Unset): return i @@ -104,8 +101,7 @@ def validate_int(b): def validate_const(v): def validate(c): - # Optional[T] is a Union[T, None] - if is_union(type(c)) and type(None) in get_args(type(c)) and c is None: + if c is None: return None if v != c: @@ -117,10 +113,12 @@ def validate(c): def unmarshal_json(raw, typ: Any) -> Any: - return unmarshal(from_json(raw), typ) + return unmarshal(from_json(raw), typ, coerce_iterables=False) -def unmarshal(val, typ: Any) -> Any: +def unmarshal(val, typ: Any, coerce_iterables: bool = True) -> Any: + if coerce_iterables: + val = _coerce_iterables_for_type(val, typ) unmarshaller = create_model( "Unmarshaller", body=(typ, ...), @@ -197,9 +195,88 @@ def get_pydantic_model(data: Any, typ: Any) -> Any: if not _contains_pydantic_model(data): return unmarshal(data, typ) + return _coerce_iterables_for_type(data, typ) + + +def _coerce_iterables_for_type(data: Any, typ: Any) -> Any: + if data is None or isinstance(data, (BaseModel, Unset)): + return data + + typ = _resolve_type_alias(typ) + origin = get_origin(typ) + + if _is_annotated_type(origin): + args = get_args(typ) + return _coerce_iterables_for_type(data, args[0]) if args else data + + if is_union(origin): + for arg in (arg for arg in get_args(typ) if arg is not type(None)): + coerced = _coerce_iterables_for_type(data, arg) + if coerced is not data: + return coerced + return data + + if _is_list_type(typ): + item_type = get_args(typ)[0] if get_args(typ) else Any + if isinstance(data, (str, bytes, bytearray, Mapping)): + return data + if isinstance(data, Iterable): + return [_coerce_iterables_for_type(item, item_type) for item in data] + return data + + if _is_mapping_type(typ): + value_type = get_args(typ)[1] if len(get_args(typ)) > 1 else Any + if isinstance(data, Mapping): + return { + key: _coerce_iterables_for_type(value, value_type) + for key, value in data.items() + } + return data + + if _is_pydantic_model_type(typ) and isinstance(data, Mapping): + coerced = None + for field_name, field in typ.model_fields.items(): + field_type = field.annotation + for key in (field_name, field.alias): + if key is not None and key in data: + value = data[key] if coerced is None else coerced[key] + coerced_value = _coerce_iterables_for_type(value, field_type) + if coerced_value is not value: + if coerced is None: + coerced = dict(data) + coerced[key] = coerced_value + return coerced if coerced is not None else data + return data +def _resolve_type_alias(typ: Any) -> Any: + return getattr(typ, "__value__", typ) + + +def _is_annotated_type(origin: Any) -> bool: + return any( + origin is typing_obj + for typing_obj in _get_typing_objects_by_name_of("Annotated") + ) + + +def _is_list_type(typ: Any) -> bool: + typ = _resolve_type_alias(typ) + return typ is list or get_origin(typ) is list + + +def _is_mapping_type(typ: Any) -> bool: + typ = _resolve_type_alias(typ) + origin = get_origin(typ) + mapping_origin = get_origin(Mapping[Any, Any]) + return typ in (dict, Dict, Mapping) or origin in (dict, Mapping, mapping_origin) + + +def _is_pydantic_model_type(typ: Any) -> bool: + return isinstance(typ, type) and issubclass(typ, BaseModel) + + def _contains_pydantic_model(data: Any) -> bool: if isinstance(data, BaseModel): return True diff --git a/tests/README.md b/tests/README.md index 7343f4e..5a8c7e3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -88,7 +88,7 @@ The `test_live.py` file contains tests that run against the real You.com API. Th ```bash # Run live tests with your API key -YOU_API_KEY_AUTH="your-api-key" pytest tests/test_live.py -v +YDC_API_KEY="your-api-key" pytest tests/test_live.py -v # Run all tests except live tests pytest tests/ --ignore=tests/test_live.py -v diff --git a/tests/mockserver/internal/handler/generated_handlers.go b/tests/mockserver/internal/handler/generated_handlers.go index 4aa0eaa..33dc314 100644 --- a/tests/mockserver/internal/handler/generated_handlers.go +++ b/tests/mockserver/internal/handler/generated_handlers.go @@ -16,5 +16,8 @@ func GeneratedHandlers(ctx context.Context, dir *logging.HTTPFileDirectory, rt * NewGeneratedHandler(ctx, http.MethodPost, "/v1/agents/runs", pathPostV1AgentsRuns(dir, rt)), NewGeneratedHandler(ctx, http.MethodPost, "/v1/contents", pathPostV1Contents(dir, rt)), NewGeneratedHandler(ctx, http.MethodPost, "/v1/research", pathPostV1Research(dir, rt)), + NewGeneratedHandler(ctx, http.MethodPost, "/v1/finance_research", pathPostV1FinanceResearch(dir, rt)), + NewGeneratedHandler(ctx, http.MethodGet, "/v1/research/{task_id}", pathGetV1Research(dir, rt)), + NewGeneratedHandler(ctx, http.MethodGet, "/v1/research/{task_id}/stream", pathGetV1ResearchStream(dir, rt)), } } diff --git a/tests/mockserver/internal/handler/pathgetv1research.go b/tests/mockserver/internal/handler/pathgetv1research.go new file mode 100644 index 0000000..54e70d1 --- /dev/null +++ b/tests/mockserver/internal/handler/pathgetv1research.go @@ -0,0 +1,111 @@ +package handler + +import ( + "encoding/json" + "fmt" + "log" + "mockserver/internal/handler/assert" + "mockserver/internal/logging" + "mockserver/internal/tracking" + "net/http" +) + +func pathGetV1Research(dir *logging.HTTPFileDirectory, rt *tracking.RequestTracker) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + test := req.Header.Get("x-speakeasy-test-name") + instanceID := req.Header.Get("x-speakeasy-test-instance-id") + + count := rt.GetRequestCount(test, instanceID) + + switch fmt.Sprintf("%s[%d]", test, count) { + case "get_/v1/research/{task_id}[0]": + dir.HandlerFunc("get_/v1/research/{task_id}", testGetV1ResearchTaskSuccess)(w, req) + case "get_/v1/research/{task_id}-not-found[0]": + testGetV1ResearchTaskNotFound(w, req) + case "get_/v1/research/{task_id}-unauthorized[0]": + testGetV1ResearchTaskUnauthorized(w, req) + case "get_/v1/research/{task_id}-forbidden[0]": + testGetV1ResearchTaskForbidden(w, req) + case "get_/v1/research/{task_id}-internal-error[0]": + testGetV1ResearchTaskInternalError(w, req) + default: + dir.HandlerFunc("get_/v1/research/{task_id}", testGetV1ResearchTaskSuccess)(w, req) + } + } +} + +// testGetV1ResearchTaskSuccess returns a TaskDetail with status "completed" +// and a populated result block, mirroring the structure used by the real API +// after a background research task finishes. +func testGetV1ResearchTaskSuccess(w http.ResponseWriter, req *http.Request) { + if err := assert.SecurityHeader(req, "X-API-Key", false); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + if err := assert.HeaderExists(req, "User-Agent"); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + respBody := map[string]interface{}{ + "id": "00000000-0000-0000-0000-000000000001", + "task_type": "research", + "status": "completed", + "created_at": "2026-07-09T00:00:00Z", + "updated_at": "2026-07-09T00:02:30Z", + "completed_at": "2026-07-09T00:02:30Z", + "error": nil, + "input": map[string]interface{}{ + "input": "Compare NVIDIA, AMD, and Intel revenue over 5 years", + "research_effort": "deep", + }, + "result": map[string]interface{}{ + "output": map[string]interface{}{ + "content": "# Mock Research Result\n\nMock result for completed background task.", + "content_type": "text", + "sources": []map[string]interface{}{ + { + "url": "https://example.com/research/1", + "title": "Mock Research Source 1", + "snippets": []string{"This is a relevant snippet from source 1."}, + }, + }, + }, + }, + } + + respBodyBytes, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "Unable to encode response body as JSON: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(respBodyBytes) +} + +func testGetV1ResearchTaskNotFound(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"detail":"Task not found"}`)) +} + +func testGetV1ResearchTaskUnauthorized(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message":"Invalid or expired API key"}`)) +} + +func testGetV1ResearchTaskForbidden(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) +} + +func testGetV1ResearchTaskInternalError(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"message":"Internal server error"}`)) +} diff --git a/tests/mockserver/internal/handler/pathgetv1researchstream.go b/tests/mockserver/internal/handler/pathgetv1researchstream.go new file mode 100644 index 0000000..892081a --- /dev/null +++ b/tests/mockserver/internal/handler/pathgetv1researchstream.go @@ -0,0 +1,121 @@ +package handler + +import ( + "encoding/json" + "fmt" + "log" + "mockserver/internal/handler/assert" + "mockserver/internal/logging" + "mockserver/internal/tracking" + "net/http" +) + +func pathGetV1ResearchStream(dir *logging.HTTPFileDirectory, rt *tracking.RequestTracker) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + test := req.Header.Get("x-speakeasy-test-name") + instanceID := req.Header.Get("x-speakeasy-test-instance-id") + + count := rt.GetRequestCount(test, instanceID) + + switch fmt.Sprintf("%s[%d]", test, count) { + case "get_/v1/research/{task_id}/stream[0]": + dir.HandlerFunc("get_/v1/research/{task_id}/stream", testGetV1ResearchStreamSuccess)(w, req) + case "get_/v1/research/{task_id}/stream-not-found[0]": + testGetV1ResearchStreamNotFound(w, req) + case "get_/v1/research/{task_id}/stream-unauthorized[0]": + testGetV1ResearchStreamUnauthorized(w, req) + case "get_/v1/research/{task_id}/stream-internal-error[0]": + testGetV1ResearchStreamInternalError(w, req) + default: + dir.HandlerFunc("get_/v1/research/{task_id}/stream", testGetV1ResearchStreamSuccess)(w, req) + } + } +} + +// testGetV1ResearchStreamSuccess emits the SSE sequence documented in the +// Research API stream spec: an opening `connected` event followed by a +// terminal `response.done` event, then a normal close. +// +// Event types match TERMINAL_SSE_EVENTS in +// `ydc_services/libs/workflows/task_shared.py` ({"response.done", "complete", +// "error", "cancelled"}). +func testGetV1ResearchStreamSuccess(w http.ResponseWriter, req *http.Request) { + if err := assert.SecurityHeader(req, "X-API-Key", false); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + if err := assert.HeaderExists(req, "User-Agent"); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // SSE requires these headers; flushing after each event lets the client + // receive events incrementally. + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("X-Accel-Buffering", "no") + w.WriteHeader(http.StatusOK) + + flusher, ok := w.(http.Flusher) + if !ok { + log.Printf("response writer does not support flushing; stream may buffer") + } + + writeSSE := func(id, event string, data map[string]interface{}) bool { + payload, err := json.Marshal(data) + if err != nil { + log.Printf("error marshalling SSE data: %s", err) + return false + } + if _, err := fmt.Fprintf(w, "id: %s\nevent: %s\ndata: %s\n\n", id, event, payload); err != nil { + return false + } + if ok { + flusher.Flush() + } + return true + } + + taskID := "00000000-0000-0000-0000-000000000001" + + // 1) Opening event — sent unconditionally when the stream is opened. + if !writeSSE("0", "connected", map[string]interface{}{ + "type": "connected", + "task_id": taskID, + "status": "running", + }) { + return + } + + // 2) Terminal event — closes the stream. `response.done` is one of the + // four TERMINAL_SSE_EVENTS values that the SDK treats as stream-end. + if !writeSSE("1", "response.done", map[string]interface{}{ + "type": "response.done", + "task_id": taskID, + "status": "completed", + "sequence": 1, + }) { + return + } +} + +func testGetV1ResearchStreamNotFound(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"detail":"Task not found"}`)) +} + +func testGetV1ResearchStreamUnauthorized(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message":"Invalid or expired API key"}`)) +} + +func testGetV1ResearchStreamInternalError(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"message":"Internal server error"}`)) +} diff --git a/tests/mockserver/internal/handler/pathpostv1financeresearch.go b/tests/mockserver/internal/handler/pathpostv1financeresearch.go new file mode 100644 index 0000000..ddfd404 --- /dev/null +++ b/tests/mockserver/internal/handler/pathpostv1financeresearch.go @@ -0,0 +1,123 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io" + "log" + "mockserver/internal/handler/assert" + "mockserver/internal/logging" + "mockserver/internal/tracking" + "net/http" +) + +func pathPostV1FinanceResearch(dir *logging.HTTPFileDirectory, rt *tracking.RequestTracker) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + test := req.Header.Get("x-speakeasy-test-name") + instanceID := req.Header.Get("x-speakeasy-test-instance-id") + + count := rt.GetRequestCount(test, instanceID) + + switch fmt.Sprintf("%s[%d]", test, count) { + case "post_/v1/finance_research[0]": + dir.HandlerFunc("post_/v1/finance_research", testPostV1FinanceResearchSuccess)(w, req) + case "post_/v1/finance_research-unauthorized[0]": + testPostV1FinanceResearchUnauthorized(w, req) + case "post_/v1/finance_research-forbidden[0]": + testPostV1FinanceResearchForbidden(w, req) + case "post_/v1/finance_research-unprocessable[0]": + testPostV1FinanceResearchUnprocessable(w, req) + case "post_/v1/finance_research-internal-error[0]": + testPostV1FinanceResearchInternalError(w, req) + default: + dir.HandlerFunc("post_/v1/finance_research", testPostV1FinanceResearchSuccess)(w, req) + } + } +} + +// Finance Research sources intentionally never include the `snippets` field +// (FinanceResearchSource uses `extra="forbid"` with only `url` and `title`). +func testPostV1FinanceResearchSuccess(w http.ResponseWriter, req *http.Request) { + if err := assert.SecurityHeader(req, "X-API-Key", false); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + if err := assert.ContentType(req, "application/json", true); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := assert.HeaderExists(req, "User-Agent"); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var requestBody map[string]interface{} + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + log.Printf("error reading request body: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := json.Unmarshal(bodyBytes, &requestBody); err != nil { + log.Printf("error parsing request body: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + effort, _ := requestBody["research_effort"].(string) + if effort == "" { + effort = "deep" + } + + respBody := map[string]interface{}{ + "output": map[string]interface{}{ + "content": fmt.Sprintf( + "# Mock Finance Research (effort: %s)\n\nNVIDIA's FY2025 revenue grew on Data Center demand.", + effort, + ), + "content_type": "text", + "sources": []map[string]interface{}{ + { + "url": "https://investor.nvidia.com/financial-info/financial-reports/default.aspx", + "title": "NVIDIA Corporation - Financial Reports", + }, + }, + }, + } + + respBodyBytes, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "Unable to encode response body as JSON: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(respBodyBytes) +} + +func testPostV1FinanceResearchUnauthorized(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message":"Invalid or expired API key"}`)) +} + +func testPostV1FinanceResearchForbidden(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) +} + +func testPostV1FinanceResearchUnprocessable(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"detail":[{"type":"missing","loc":["body","input"],"msg":"Field required","input":""}]}`)) +} + +func testPostV1FinanceResearchInternalError(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"message":"Internal server error"}`)) +} diff --git a/tests/mockserver/internal/handler/pathpostv1research.go b/tests/mockserver/internal/handler/pathpostv1research.go index 11cd0a0..71a8b54 100644 --- a/tests/mockserver/internal/handler/pathpostv1research.go +++ b/tests/mockserver/internal/handler/pathpostv1research.go @@ -21,6 +21,8 @@ func pathPostV1Research(dir *logging.HTTPFileDirectory, rt *tracking.RequestTrac switch fmt.Sprintf("%s[%d]", test, count) { case "post_/v1/research[0]": dir.HandlerFunc("post_/v1/research", testPostV1ResearchSuccess)(w, req) + case "post_/v1/research-background[0]": + dir.HandlerFunc("post_/v1/research-background", testPostV1ResearchBackground)(w, req) case "post_/v1/research-unauthorized[0]": testPostV1ResearchUnauthorized(w, req) case "post_/v1/research-forbidden[0]": @@ -71,6 +73,13 @@ func testPostV1ResearchSuccess(w http.ResponseWriter, req *http.Request) { effort = "standard" } + // When `background: true` is set, return a TaskResponse shape so the SDK + // can deserialize the async task handle instead of an inline ResearchResponse. + if background, _ := requestBody["background"].(bool); background { + respondTaskResponse(w, "research", "queued", "/v1/research/00000000-0000-0000-0000-000000000001") + return + } + respBody := map[string]interface{}{ "output": map[string]interface{}{ "content": fmt.Sprintf("# Mock Research Response\n\nThis is a mock research response for: %s (effort: %s)\n\nQuantum computing has seen significant advances in recent years.", input, effort), @@ -100,6 +109,40 @@ func testPostV1ResearchSuccess(w http.ResponseWriter, req *http.Request) { _, _ = w.Write(respBodyBytes) } +// respondTaskResponse writes a TaskResponse payload used by background-mode +// research. Kept here so the GET handler below can reuse the same shape. +func respondTaskResponse(w http.ResponseWriter, typeValue, statusValue, streamPathSuffix string) { + respBody := map[string]interface{}{ + "task_id": "00000000-0000-0000-0000-000000000001", + "type": typeValue, + "status": statusValue, + "stream_url": streamPathSuffix, + "created_at": "2026-07-09T00:00:00Z", + } + respBodyBytes, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "Unable to encode response body as JSON: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(respBodyBytes) +} + +func testPostV1ResearchBackground(w http.ResponseWriter, req *http.Request) { + if err := assert.SecurityHeader(req, "X-API-Key", false); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + if err := assert.ContentType(req, "application/json", true); err != nil { + log.Printf("assertion error: %s\n", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + respondTaskResponse(w, "research", "queued", "/v1/research/00000000-0000-0000-0000-000000000001/stream") +} + func testPostV1ResearchUnauthorized(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) diff --git a/tests/test_contents.py b/tests/test_contents.py index 1cc469c..6127289 100644 --- a/tests/test_contents.py +++ b/tests/test_contents.py @@ -17,7 +17,7 @@ def server_url(): @pytest.fixture def api_key(): - return os.getenv("YOU_API_KEY_AUTH", "test-api-key") + return "test-api-key" class TestContentsBasic: diff --git a/tests/test_live.py b/tests/test_live.py index 2093180..02b0da1 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -2,9 +2,9 @@ Live API tests for You.com Python SDK. These tests run against the real You.com API to verify SDK functionality. -Set the YOU_API_KEY_AUTH environment variable before running: +Set the YDC_API_KEY environment variable before running: - YOU_API_KEY_AUTH="your-api-key" pytest tests/test_live.py -v + YDC_API_KEY="your-api-key" pytest tests/test_live.py -v To skip these tests, run pytest with the --ignore flag: pytest tests/ --ignore=tests/test_live.py -v @@ -34,15 +34,15 @@ # Skip all tests in this file if no API key is provided pytestmark = pytest.mark.skipif( - not os.getenv("YOU_API_KEY_AUTH"), - reason="YOU_API_KEY_AUTH environment variable not set" + not os.getenv("YDC_API_KEY"), + reason="YDC_API_KEY environment variable not set" ) @pytest.fixture def api_key(): """Get API key from environment.""" - return os.getenv("YOU_API_KEY_AUTH") + return os.getenv("YDC_API_KEY") @pytest.fixture diff --git a/tests/test_performance.py b/tests/test_performance.py index 41b0a1c..5e958eb 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -386,7 +386,7 @@ def call(): query="AI research", count=3, livecrawl=LiveCrawl.WEB, - livecrawl_formats=LiveCrawlFormats.HTML, + livecrawl_formats=[LiveCrawlFormats.HTML], server_url=server_url, ) @@ -405,7 +405,7 @@ def call(): query="documentation guides", count=3, livecrawl=LiveCrawl.WEB, - livecrawl_formats=LiveCrawlFormats.MARKDOWN, + livecrawl_formats=[LiveCrawlFormats.MARKDOWN], server_url=server_url, ) @@ -448,7 +448,7 @@ def call(): freshness=Freshness.WEEK, country=Country.GB, livecrawl=LiveCrawl.WEB, - livecrawl_formats=LiveCrawlFormats.MARKDOWN, + livecrawl_formats=[LiveCrawlFormats.MARKDOWN], server_url=server_url, ) @@ -467,7 +467,7 @@ def call(): query="technology news", count=5, livecrawl=LiveCrawl.NEWS, - livecrawl_formats=LiveCrawlFormats.MARKDOWN, + livecrawl_formats=[LiveCrawlFormats.MARKDOWN], server_url=server_url, ) @@ -486,7 +486,7 @@ def call(): query="breaking tech news", count=3, livecrawl=LiveCrawl.ALL, - livecrawl_formats=LiveCrawlFormats.HTML, + livecrawl_formats=[LiveCrawlFormats.HTML], server_url=server_url, ) diff --git a/tests/test_research.py b/tests/test_research.py index 793a617..e6203df 100644 --- a/tests/test_research.py +++ b/tests/test_research.py @@ -7,15 +7,19 @@ from tests.test_client import create_test_http_client from youdotcom import You from youdotcom.errors import ( + FinanceResearchUnauthorizedError, ResearchForbiddenError, ResearchInternalServerError, ResearchUnauthorizedError, - UnprocessableEntityError, + ResearchUnprocessableEntityError, + UnprocessableEntityResponseError, YouDefaultError, ) from youdotcom.models import ( + FinanceResearchEffort, ResearchEffort, ResearchResponse, + TaskResponse, ) @@ -26,7 +30,7 @@ def server_url(): @pytest.fixture def api_key(): - return os.getenv("YOU_API_KEY_AUTH", "test-api-key") + return "test-api-key" class TestResearchBasic: @@ -155,7 +159,7 @@ def test_unprocessable_entity(self, server_url, api_key): client = create_test_http_client("post_/v1/research-unprocessable") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - with pytest.raises((UnprocessableEntityError, YouDefaultError)): + with pytest.raises((ResearchUnprocessableEntityError, UnprocessableEntityResponseError, YouDefaultError)): you.research( input="", server_url=server_url, @@ -170,3 +174,206 @@ def test_internal_server_error(self, server_url, api_key): input="test", server_url=server_url, ) + + +class TestFinanceResearch: + def test_basic_finance_research(self, server_url, api_key): + client = create_test_http_client("post_/v1/finance_research") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.finance_research( + input="What was NVIDIA's FY2025 revenue?", + research_effort=FinanceResearchEffort.DEEP, + server_url=server_url, + ) + + assert res.output is not None + assert res.output.content is not None + assert "NVIDIA" in res.output.content + # Finance sources intentionally never include the `snippets` field. + assert res.output.sources is not None + assert len(res.output.sources) > 0 + for source in res.output.sources: + assert source.url is not None + assert source.title is not None + + def test_finance_research_unauthorized(self, server_url): + client = create_test_http_client("post_/v1/finance_research-unauthorized") + + with You(server_url=server_url, client=client, api_key_auth="invalid") as you: + with pytest.raises((FinanceResearchUnauthorizedError, YouDefaultError)): + you.finance_research( + input="test", + server_url=server_url, + ) + + +class TestResearchBackground: + """Direct coverage for the auto-generated background/stream SDK methods.""" + + def test_research_background_returns_task_response(self, server_url, api_key): + client = create_test_http_client("post_/v1/research-background") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.research( + input="What is the capital of France?", + research_effort=ResearchEffort.STANDARD, + background=True, + server_url=server_url, + ) + + assert isinstance(res, TaskResponse) + assert res.task_id == "00000000-0000-0000-0000-000000000001" + assert res.status.value == "queued" + assert res.stream_url is not None + assert res.created_at is not None + + def test_get_research_task_returns_task_detail(self, server_url, api_key): + client = create_test_http_client("get_/v1/research/{task_id}") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + detail = you.get_research_task( + task_id="00000000-0000-0000-0000-000000000001", + server_url=server_url, + ) + + assert detail.id == "00000000-0000-0000-0000-000000000001" + assert detail.task_type == "research" + assert detail.status.value == "completed" + assert detail.created_at is not None + assert detail.completed_at is not None + # result is populated server-side; the typed Result model itself + # has no fields (extra=ignore), so we only assert presence here. + assert detail.result is not None + + +# --------------------------------------------------------------------------- +# 2.4.0 beta params: output_schema (structured output_content) and +# source_control (domain constraints / freshness / country). +# --------------------------------------------------------------------------- + +import json + + +class TestResearchOutputSchema: + """``output_schema=`` request flips ``output.content_type`` to ``object`` + and the server-side `output.content` becomes a structured JSON object + matching the schema. + + The default Go mockserver returns ``content_type=text`` regardless of + the request body, so this test uses ``httpx.MockTransport`` to inject a + realistic server response with ``content_type=object`` and asserts the + SDK correctly deserializes the ``content_type`` slot. + + Caveat (2.4.0): the typed ``Content`` model in `researchresponse.py` + declares no fields and uses pydantic's default ``extra="ignore"``, so + the unknown ``content`` payload is dropped at unmarshal time. Today + `res.output.content` becomes an empty ``Content()`` instead of the + structured dict, same root cause as the ``TaskDetail.result`` + empty-dict problem called out in `research_helpers.py` and + CHANGELOG.md. This test asserts the documented behavior (content_type + slot flips) without trying to recover the lost structure. + """ + + def test_output_schema_sets_content_type_to_object(self, server_url, api_key): + structured_payload = { + "same_entity": True, + "confidence": 0.95, + "evidence": ["https://acme-logistics.com/about"], + } + + def handler(request): + body = json.loads(request.content) + assert "output_schema" in body + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "output": { + "content": structured_payload, + "content_type": "object", + "sources": [], + }, + }), + ) + + transport = httpx.MockTransport(handler) + sdk_client = httpx.Client(transport=transport) + you = You( + server_url=server_url, + client=sdk_client, + api_key_auth=api_key, + ) + + res = you.research( + input="Are 'Acme Logistics LLC' (Delaware) and 'Acme Logistics' (Newark, NJ) the same business?", + research_effort=ResearchEffort.STANDARD, + output_schema={ + "type": "object", + "properties": { + "same_entity": {"type": "boolean"}, + "confidence": {"type": "number"}, + "evidence": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["same_entity", "confidence", "evidence"], + "additionalProperties": False, + }, + ) + + assert isinstance(res, ResearchResponse) + assert res.output.content_type.value == "object" + # SDK-level caveat: typed Content model has no fields, so + # res.output.content is an empty Content() instance rather than the + # structured dict. Document and assert the documented shape. + assert res.output.content is not None + dumped = res.output.content.model_dump() + assert dumped == {} or isinstance(res.output.content, str) + + +class TestResearchSourceControl: + """``source_control=`` accepts the documented shape (include_domains, + exclude_domains, boost_domains, freshness, country) and is forwarded + to the server. The Go mockserver doesn't validate the request body, + so this is an end-to-end smoke test that all five fields serialize + and round-trip without complaint. + """ + + def test_source_control_with_include_domains(self, server_url, api_key): + """include_domains alone is valid per the SDK docstring.""" + client = create_test_http_client("post_/v1/research") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.research( + input="What did the Federal Reserve do in 2024?", + research_effort=ResearchEffort.STANDARD, + source_control={ + "include_domains": ["federalreserve.gov"], + }, + server_url=server_url, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + + def test_source_control_with_boost_and_exclude(self, server_url, api_key): + """boost_domains + exclude_domains is the only valid two-way pair.""" + client = create_test_http_client("post_/v1/research") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.research( + input="What happened in the AI industry this year?", + research_effort=ResearchEffort.STANDARD, + source_control={ + "boost_domains": ["nytimes.com", "wired.com"], + "exclude_domains": ["reddit.com"], + "country": "US", + "freshness": "month", + }, + server_url=server_url, + ) + + assert isinstance(res, ResearchResponse) + assert res.output is not None + + + diff --git a/tests/test_research_helpers.py b/tests/test_research_helpers.py new file mode 100644 index 0000000..232855a --- /dev/null +++ b/tests/test_research_helpers.py @@ -0,0 +1,682 @@ +"""Tests for research background-mode helpers in youdotcom.research_helpers.""" + +import os +import uuid + +import httpx +import pytest + +from tests.test_client import create_test_http_client +from youdotcom import You +from youdotcom.models import ( + ResearchEffort, + TaskDetail, + TaskResponse, +) +from youdotcom.research_helpers import ( + RawStreamEvent, + research_and_wait, + research_and_wait_async, + research_background, + research_background_async, + poll_research_task, + poll_research_task_async, + stream_research_events_raw, + stream_research_events_raw_async, + _decode_raw_event, +) + + +@pytest.fixture +def server_url(): + return os.getenv("TEST_SERVER_URL", "http://localhost:18080") + + +@pytest.fixture +def api_key(): + return "test-api-key" + + +# --------------------------------------------------------------------------- +# research_background[Async]: assert TaskResponse return type without +# forcing callers to narrow Union[ResearchResponse, TaskResponse]. +# --------------------------------------------------------------------------- + +class TestResearchBackground: + def test_research_background_returns_task_response(self, server_url, api_key): + client = create_test_http_client("post_/v1/research-background") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = research_background( + you, + input="Compare NVIDIA, AMD, and Intel revenue over 5 years", + research_effort=ResearchEffort.DEEP, + server_url=server_url, + ) + + assert isinstance(res, TaskResponse) + assert res.task_id == "00000000-0000-0000-0000-000000000001" + assert res.type == "research" + assert res.status.value == "queued" + + @pytest.mark.asyncio + async def test_research_background_async_returns_task_response(self, server_url, api_key): + async_client = httpx.AsyncClient( + headers={ + "x-speakeasy-test-name": "post_/v1/research-background", + "x-speakeasy-test-instance-id": str(uuid.uuid4()), + }, + follow_redirects=True, + ) + + async with You( + server_url=server_url, async_client=async_client, api_key_auth=api_key + ) as you: + res = await research_background_async( + you, + input="Compare NVIDIA, AMD, and Intel revenue over 5 years", + research_effort=ResearchEffort.DEEP, + server_url=server_url, + ) + + assert isinstance(res, TaskResponse) + assert res.task_id == "00000000-0000-0000-0000-000000000001" + + +# --------------------------------------------------------------------------- +# poll_research_task[Async]: poll get_research_task until terminal and +# return the completed TaskDetail with result payload. +# --------------------------------------------------------------------------- + +class TestPollResearchTask: + def test_poll_returns_completed_task_detail(self, server_url, api_key): + client = create_test_http_client("get_/v1/research/{task_id}") + + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + detail = poll_research_task( + you, + "00000000-0000-0000-0000-000000000001", + interval_s=0.01, + timeout_s=2.0, + ) + + assert isinstance(detail, TaskDetail) + assert detail.status.value == "completed" + assert detail.result is not None + + @pytest.mark.asyncio + async def test_poll_async_returns_completed_task_detail(self, server_url, api_key): + async_client = httpx.AsyncClient( + headers={ + "x-speakeasy-test-name": "get_/v1/research/{task_id}", + "x-speakeasy-test-instance-id": str(uuid.uuid4()), + }, + follow_redirects=True, + ) + + async with You( + server_url=server_url, async_client=async_client, api_key_auth=api_key + ) as you: + detail = await poll_research_task_async( + you, + "00000000-0000-0000-0000-000000000001", + interval_s=0.01, + timeout_s=2.0, + ) + + assert isinstance(detail, TaskDetail) + assert detail.status.value == "completed" + + +# --------------------------------------------------------------------------- +# research_and_wait: submit background + poll + return TaskDetail. +# Note: result.output cannot be cleanly typed today (Result.extra=ignore), +# so the helper returns TaskDetail; the test asserts the contract explicitly. +# --------------------------------------------------------------------------- + +class TestResearchAndWait: + def test_and_wait_poll_returns_completed_detail(self, server_url, api_key): + client_post = create_test_http_client("post_/v1/research-background") + + with You(server_url=server_url, client=client_post, api_key_auth=api_key) as you: + detail = research_and_wait( + you, + mode="poll", + interval_s=0.01, + timeout_s=2.0, + server_url=server_url, + input="Compare NVIDIA, AMD, and Intel revenue over 5 years", + research_effort=ResearchEffort.DEEP, + ) + + assert isinstance(detail, TaskDetail) + assert detail.status.value == "completed" + + +# --------------------------------------------------------------------------- +# Tolerant SSE stream raw events. Verifies: +# (a) helper accepts documented event types as before; +# (b) helper accepts unknown event types without raising +# pydantic.ValidationError, surfacing them as RawStreamEvent(event="..."). +# Uses an httpx.MockTransport to inject a fake SSE server response -- this +# path doesn't depend on the Go mockserver so unknown event names can be +# emitted safely without adding new mockserver fixtures. +# --------------------------------------------------------------------------- + +class TestStreamResearchEventsTolerant: + def test_tolerant_stream_yields_all_events_with_unknown_name(self): + # Inject a fake SSE stream with one workflow-internal event type + # (research.searching) that's NOT in the documented enum. The strict + # speakeasy decoder would raise ValidationError on it; our tolerant + # helper must surface it as RawStreamEvent(event="research.searching"). + recorded_ua: dict = {} + + def record_send(request): + recorded_ua["value"] = request.headers.get("User-Agent") + chunks = [ + b"id: 0\nevent: connected\ndata: " + b'{"type":"connected","task_id":"abc","status":"running"}\n\n', + b"id: 1\nevent: research.searching\ndata: " + b'{"query":"markets","phase":"searching"}\n\n', + b"id: 2\nevent: response.done\ndata: " + b'{"type":"response.done","task_id":"abc","status":"completed","sequence":2}\n\n', + ] + return httpx.Response( + 200, + headers={"content-type": "text/event-stream"}, + content=chunks, + ) + + transport = httpx.MockTransport(record_send) + sdk_client = httpx.Client( + transport=transport, + headers={ + "x-speakeasy-test-name": "get_/v1/research/{task_id}/stream", + "x-speakeasy-test-instance-id": str(uuid.uuid4()), + }, + ) + you = You( + server_url="http://mock.local", + client=sdk_client, + api_key_auth="test-api-key", + ) + + events = list( + stream_research_events_raw( + you, "00000000-0000-0000-0000-000000000001", + ) + ) + + assert [e.event for e in events] == [ + "connected", "research.searching", "response.done", + ] + # Validate that the unknown event came through without raising. + assert isinstance(events[1], RawStreamEvent) + assert events[1].data == {"query": "markets", "phase": "searching"} + # And confirm the SDK still set User-Agent on the underlying request + # (the YDCUserAgentOverrideHook ran before send). + assert recorded_ua["value"] == f"youdotcom-python-sdk/{you.sdk_configuration.sdk_version}" + + +# --------------------------------------------------------------------------- +# YDCUserAgentOverrideHook must respect sdk_configuration.user_agent +# overrides instead of unconditionally rewriting the header. +# --------------------------------------------------------------------------- + +class TestYDCUserAgentOverrideHook: + def test_default_user_agent_uses_youdotcom_format(self, server_url): + # Default flow: hook returns youdotcom-python-sdk/. + client = create_test_http_client("post_/v1/research") + + you = You(server_url=server_url, client=client, api_key_auth="test") + # The default config.user_agent is the speakeasy-encoded value. + # Build a synthetic request and run the hook against it. + from youdotcom._hooks.registration import YDCUserAgentOverrideHook + from youdotcom._hooks.types import HookContext + + request = httpx.Request( + "POST", f"{server_url}/v1/research", + json={"input": "hello"}, + ) + hook = YDCUserAgentOverrideHook() + hook_ctx = HookContext( + config=you.sdk_configuration, + base_url=server_url, + operation_id="post_/v1/research", + oauth2_scopes=None, + security_source=None, + tags=None, + extensions=None, + ) + result = hook.before_request(hook_ctx, request) + assert result.headers.get("User-Agent") == f"youdotcom-python-sdk/{you.sdk_configuration.sdk_version}" + + def test_custom_user_agent_is_passthrough(self, server_url): + client = create_test_http_client("post_/v1/research") + you = You(server_url=server_url, client=client, api_key_auth="test") + you.sdk_configuration.user_agent = "youdotcom-temporal/0.1.0" + + from youdotcom._hooks.registration import YDCUserAgentOverrideHook + from youdotcom._hooks.types import HookContext + + request = httpx.Request( + "POST", f"{server_url}/v1/research", + json={"input": "hello"}, + ) + hook = YDCUserAgentOverrideHook() + hook_ctx = HookContext( + config=you.sdk_configuration, + base_url=server_url, + operation_id="post_/v1/research", + oauth2_scopes=None, + security_source=None, + tags=None, + extensions=None, + ) + result = hook.before_request(hook_ctx, request) + assert result.headers.get("User-Agent") == "youdotcom-temporal/0.1.0" + + +# --------------------------------------------------------------------------- +# _decode_raw_event sanity: known and unknown shapes. +# --------------------------------------------------------------------------- + +class TestDecodeRawEvent: + def test_known_event(self): + import json + ev = _decode_raw_event(json.dumps({"id": "1", "event": "response.done", "data": {"status": "completed"}})) + assert isinstance(ev, RawStreamEvent) + assert ev.id == "1" + assert ev.event == "response.done" + assert ev.data == {"status": "completed"} + + def test_unknown_event(self): + import json + ev = _decode_raw_event(json.dumps({"id": "2", "event": "some.workflow.step", "data": {"k": "v"}})) + assert ev.event == "some.workflow.step" + assert ev.data == {"k": "v"} + + +# --------------------------------------------------------------------------- +# Error path tests: poll timeout, poll failed status, and stream mode. +# --------------------------------------------------------------------------- + +class TestPollResearchTaskErrorPaths: + def test_poll_timeout_raises_timeout_error(self): + """poll_research_task must raise TimeoutError when the task never + reaches a terminal state within timeout_s.""" + import json + + def handler(request): + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "id": "00000000-0000-0000-0000-000000000001", + "task_type": "research", + "status": "running", + "created_at": "2026-07-09T00:00:00Z", + "updated_at": "2026-07-09T00:00:01Z", + }), + ) + + transport = httpx.MockTransport(handler) + sdk_client = httpx.Client(transport=transport) + you = You( + server_url="http://mock.local", + client=sdk_client, + api_key_auth="test-api-key", + ) + + with pytest.raises(TimeoutError, match="did not complete"): + poll_research_task( + you, + "00000000-0000-0000-0000-000000000001", + interval_s=0.01, + timeout_s=0.05, + ) + + def test_poll_failed_status_raises_runtime_error(self): + """poll_research_task must raise RuntimeError when the task ends + in a non-completed terminal state (failed).""" + import json + + def handler(request): + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "id": "00000000-0000-0000-0000-000000000002", + "task_type": "research", + "status": "failed", + "created_at": "2026-07-09T00:00:00Z", + "updated_at": "2026-07-09T00:00:05Z", + "error": "upstream search timeout", + }), + ) + + transport = httpx.MockTransport(handler) + sdk_client = httpx.Client(transport=transport) + you = You( + server_url="http://mock.local", + client=sdk_client, + api_key_auth="test-api-key", + ) + + with pytest.raises(RuntimeError, match="non-completed state: failed"): + poll_research_task( + you, + "00000000-0000-0000-0000-000000000002", + interval_s=0.01, + timeout_s=2.0, + ) + + +class TestResearchAndWaitStreamMode: + def test_and_wait_stream_returns_completed_detail(self): + """research_and_wait with mode='stream' must read the SSE stream + until response.done, then fetch the final TaskDetail via poll.""" + import json + + call_log: list = [] + + def handler(request): + url = str(request.url) + if url.endswith("/stream") or "/stream?" in url: + call_log.append("stream") + chunks = [ + b'id: 0\nevent: connected\ndata: {"type":"connected","task_id":"abc","status":"running"}\n\n', + b'id: 1\nevent: response.done\ndata: {"type":"response.done","task_id":"abc","status":"completed","sequence":1}\n\n', + ] + return httpx.Response( + 200, + headers={"content-type": "text/event-stream"}, + content=chunks, + ) + if request.method == "POST" and "/v1/research" in url and "/stream" not in url: + call_log.append("submit") + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "task_id": "00000000-0000-0000-0000-000000000001", + "type": "research", + "status": "queued", + "stream_url": "/v1/research/00000000-0000-0000-0000-000000000001/stream", + "created_at": "2026-07-09T00:00:00Z", + }), + ) + # GET /v1/research/{task_id} — final poll after stream + call_log.append("poll") + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "id": "00000000-0000-0000-0000-000000000001", + "task_type": "research", + "status": "completed", + "created_at": "2026-07-09T00:00:00Z", + "updated_at": "2026-07-09T00:02:30Z", + "completed_at": "2026-07-09T00:02:30Z", + "result": {"output": {"content": "done", "content_type": "text", "sources": []}}, + }), + ) + + transport = httpx.MockTransport(handler) + sdk_client = httpx.Client(transport=transport) + you = You( + server_url="http://mock.local", + client=sdk_client, + api_key_auth="test-api-key", + ) + + detail = research_and_wait( + you, + mode="stream", + interval_s=0.01, + timeout_s=5.0, + input="test query", + research_effort=ResearchEffort.STANDARD, + ) + + assert isinstance(detail, TaskDetail) + assert detail.status.value == "completed" + # Verify the call sequence: submit -> stream -> poll + assert call_log == ["submit", "stream", "poll"] + + +class TestPollResearchTaskAsyncErrorPaths: + @pytest.mark.asyncio + async def test_poll_async_timeout_raises_timeout_error(self): + """Async mirror of the sync timeout test.""" + import json + + def handler(request): + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "id": "00000000-0000-0000-0000-000000000003", + "task_type": "research", + "status": "running", + "created_at": "2026-07-09T00:00:00Z", + "updated_at": "2026-07-09T00:00:01Z", + }), + ) + + transport = httpx.MockTransport(handler) + sdk_async_client = httpx.AsyncClient(transport=transport) + you = You( + server_url="http://mock.local", + async_client=sdk_async_client, + api_key_auth="test-api-key", + ) + + with pytest.raises(TimeoutError, match="did not complete"): + await poll_research_task_async( + you, + "00000000-0000-0000-0000-000000000003", + interval_s=0.01, + timeout_s=0.05, + ) + + @pytest.mark.asyncio + async def test_poll_async_failed_status_raises_runtime_error(self): + """Async mirror of the sync failed-status test.""" + import json + + def handler(request): + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "id": "00000000-0000-0000-0000-000000000004", + "task_type": "research", + "status": "failed", + "created_at": "2026-07-09T00:00:00Z", + "updated_at": "2026-07-09T00:00:05Z", + "error": "upstream search timeout", + }), + ) + + transport = httpx.MockTransport(handler) + sdk_async_client = httpx.AsyncClient(transport=transport) + you = You( + server_url="http://mock.local", + async_client=sdk_async_client, + api_key_auth="test-api-key", + ) + + with pytest.raises(RuntimeError, match="non-completed state: failed"): + await poll_research_task_async( + you, + "00000000-0000-0000-0000-000000000004", + interval_s=0.01, + timeout_s=2.0, + ) + + +# --------------------------------------------------------------------------- +# Async streaming: mirror the sync TestStreamResearchEventsTolerant and +# TestResearchAndWaitStreamMode. These also exercise the try/finally cleanup +# path (replacing the broken contextlib.aclosing that called aclose()). + +class _AsyncChunks(httpx.AsyncByteStream): + """Wrap a list of bytes chunks in an AsyncByteStream for MockTransport + + AsyncClient streaming responses (httpx MockTransport returns sync + streams by default, which AsyncClient rejects for stream=True).""" + + def __init__(self, chunks: list[bytes]): + self._chunks = chunks + + async def __aiter__(self): + for chunk in self._chunks: + yield chunk + + +class TestStreamResearchEventsTolerantAsync: + @pytest.mark.asyncio + async def test_async_tolerant_stream_yields_all_events_with_unknown_name(self): + """Async mirror of TestStreamResearchEventsTolerant — injects a + fake SSE stream with an unknown event type and verifies it surfaces + as RawStreamEvent without raising.""" + recorded_ua: dict = {} + + def record_send(request): + recorded_ua["value"] = request.headers.get("User-Agent") + chunks = [ + b"id: 0\nevent: connected\ndata: " + b'{"type":"connected","task_id":"abc","status":"running"}\n\n', + b"id: 1\nevent: research.searching\ndata: " + b'{"query":"markets","phase":"searching"}\n\n', + b"id: 2\nevent: response.done\ndata: " + b'{"type":"response.done","task_id":"abc","status":"completed","sequence":2}\n\n', + ] + return httpx.Response( + 200, + headers={"content-type": "text/event-stream"}, + stream=_AsyncChunks(chunks), + ) + + transport = httpx.MockTransport(record_send) + sdk_async_client = httpx.AsyncClient(transport=transport) + you = You( + server_url="http://mock.local", + async_client=sdk_async_client, + api_key_auth="test-api-key", + ) + + events = [ + evt + async for evt in stream_research_events_raw_async( + you, "00000000-0000-0000-0000-000000000001", + ) + ] + + assert [e.event for e in events] == [ + "connected", "research.searching", "response.done", + ] + assert isinstance(events[1], RawStreamEvent) + assert events[1].data == {"query": "markets", "phase": "searching"} + assert recorded_ua["value"] == f"youdotcom-python-sdk/{you.sdk_configuration.sdk_version}" + + +class TestResearchAndWaitStreamModeAsync: + @pytest.mark.asyncio + async def test_async_and_wait_stream_returns_completed_detail(self): + """Async mirror of TestResearchAndWaitStreamMode — verifies the + submit -> stream -> poll sequence and that the stream cleanup + (try/finally close) does not raise.""" + import json + + call_log: list = [] + + def handler(request): + url = str(request.url) + if url.endswith("/stream") or "/stream?" in url: + call_log.append("stream") + chunks = [ + b'id: 0\nevent: connected\ndata: {"type":"connected","task_id":"abc","status":"running"}\n\n', + b'id: 1\nevent: response.done\ndata: {"type":"response.done","task_id":"abc","status":"completed","sequence":1}\n\n', + ] + return httpx.Response( + 200, + headers={"content-type": "text/event-stream"}, + stream=_AsyncChunks(chunks), + ) + if request.method == "POST" and "/v1/research" in url and "/stream" not in url: + call_log.append("submit") + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "task_id": "00000000-0000-0000-0000-000000000001", + "type": "research", + "status": "queued", + "stream_url": "/v1/research/00000000-0000-0000-0000-000000000001/stream", + "created_at": "2026-07-09T00:00:00Z", + }), + ) + call_log.append("poll") + return httpx.Response( + 200, + headers={"content-type": "application/json"}, + content=json.dumps({ + "id": "00000000-0000-0000-0000-000000000001", + "task_type": "research", + "status": "completed", + "created_at": "2026-07-09T00:00:00Z", + "updated_at": "2026-07-09T00:02:30Z", + "completed_at": "2026-07-09T00:02:30Z", + "result": {"output": {"content": "done", "content_type": "text", "sources": []}}, + }), + ) + + transport = httpx.MockTransport(handler) + sdk_async_client = httpx.AsyncClient(transport=transport) + you = You( + server_url="http://mock.local", + async_client=sdk_async_client, + api_key_auth="test-api-key", + ) + + detail = await research_and_wait_async( + you, + mode="stream", + interval_s=0.01, + timeout_s=5.0, + input="test query", + research_effort=ResearchEffort.STANDARD, + ) + + assert isinstance(detail, TaskDetail) + assert detail.status.value == "completed" + assert call_log == ["submit", "stream", "poll"] + + +class TestModeValidation: + def test_invalid_mode_raises_value_error(self): + """research_and_wait with an invalid mode must raise ValueError + before making any HTTP request.""" + transport = httpx.MockTransport(lambda req: httpx.Response(500)) + sdk_client = httpx.Client(transport=transport) + you = You( + server_url="http://mock.local", + client=sdk_client, + api_key_auth="test-api-key", + ) + with pytest.raises(ValueError, match="mode must be 'poll' or 'stream'"): + research_and_wait(you, mode="bogus", input="test") + + @pytest.mark.asyncio + async def test_invalid_mode_raises_value_error_async(self): + """Async mirror — research_and_wait_async with invalid mode.""" + transport = httpx.MockTransport(lambda req: httpx.Response(500)) + sdk_async_client = httpx.AsyncClient(transport=transport) + you = You( + server_url="http://mock.local", + async_client=sdk_async_client, + api_key_auth="test-api-key", + ) + with pytest.raises(ValueError, match="mode must be 'poll' or 'stream'"): + await research_and_wait_async(you, mode="bogus", input="test") diff --git a/tests/test_runs.py b/tests/test_runs.py index 0bd43ba..3a949f9 100644 --- a/tests/test_runs.py +++ b/tests/test_runs.py @@ -29,7 +29,7 @@ def server_url(): @pytest.fixture def api_key(): - return os.getenv("YOU_API_KEY_AUTH", "test-api-key") + return "test-api-key" class TestExpressAgent: diff --git a/tests/test_search.py b/tests/test_search.py index a56639c..72374df 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -4,8 +4,9 @@ from tests.test_client import create_test_http_client from youdotcom import You from youdotcom.errors import ( - SearchForbiddenError, - SearchUnauthorizedError, + ForbiddenResponseError, + UnauthorizedResponseError, + YouDefaultError, ) from youdotcom.models import ( Country, @@ -23,7 +24,7 @@ def server_url(): @pytest.fixture def api_key(): - return os.getenv("YOU_API_KEY_AUTH", "test-api-key") + return "test-api-key" class TestSearchBasic: @@ -72,13 +73,13 @@ def test_search_with_pagination(self, server_url, api_key): def test_search_with_livecrawl(self, server_url, api_key): client = create_test_http_client("get_/v1/search") - + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: res = you.search.unified( query="machine learning tutorials", count=3, livecrawl=LiveCrawl.WEB, - livecrawl_formats=LiveCrawlFormats.MARKDOWN, + livecrawl_formats=[LiveCrawlFormats.MARKDOWN], server_url=server_url, ) @@ -91,7 +92,7 @@ def test_search_with_livecrawl(self, server_url, api_key): def test_search_all_parameters(self, server_url, api_key): client = create_test_http_client("get_/v1/search") - + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: res = you.search.unified( query="quantum computing", @@ -101,7 +102,7 @@ def test_search_all_parameters(self, server_url, api_key): country=Country.GB, safesearch=SafeSearch.STRICT, livecrawl=LiveCrawl.WEB, - livecrawl_formats=LiveCrawlFormats.HTML, + livecrawl_formats=[LiveCrawlFormats.HTML], server_url=server_url, ) @@ -116,13 +117,13 @@ def test_search_all_parameters(self, server_url, api_key): def test_search_news_with_livecrawl(self, server_url, api_key): """Test that news results can have contents when livecrawl is enabled (new in 2.2.0).""" client = create_test_http_client("get_/v1/search") - + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: res = you.search.unified( query="technology news", count=5, livecrawl=LiveCrawl.NEWS, - livecrawl_formats=LiveCrawlFormats.MARKDOWN, + livecrawl_formats=[LiveCrawlFormats.MARKDOWN], server_url=server_url, ) @@ -139,13 +140,13 @@ def test_search_news_with_livecrawl(self, server_url, api_key): def test_search_livecrawl_all_with_news_contents(self, server_url, api_key): """Test livecrawl=ALL returns contents for both web and news results.""" client = create_test_http_client("get_/v1/search") - + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: res = you.search.unified( query="breaking tech news", count=3, livecrawl=LiveCrawl.ALL, - livecrawl_formats=LiveCrawlFormats.HTML, + livecrawl_formats=[LiveCrawlFormats.HTML], server_url=server_url, ) @@ -168,12 +169,12 @@ def test_unauthorized(self, server_url): client = create_test_http_client("get_/v1/search-unauthorized") with You(server_url=server_url, client=client, api_key_auth="invalid") as you: - with pytest.raises((SearchUnauthorizedError, SearchForbiddenError)): + with pytest.raises((UnauthorizedResponseError, ForbiddenResponseError, YouDefaultError)): you.search.unified(query="test", server_url=server_url) def test_forbidden(self, server_url, api_key): client = create_test_http_client("get_/v1/search-forbidden") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - with pytest.raises(SearchForbiddenError): + with pytest.raises((ForbiddenResponseError, YouDefaultError)): you.search.unified(query="test", server_url=server_url) diff --git a/tests/test_security_env.py b/tests/test_security_env.py new file mode 100644 index 0000000..a56cb22 --- /dev/null +++ b/tests/test_security_env.py @@ -0,0 +1,55 @@ +"""Tests for the hand-applied env-var precedence in `get_security_from_env`. + +`get_security_from_env` reads `YDC_API_KEY` first and falls back to +`YOU_API_KEY_AUTH` for backward compatibility with the 2.3.x env-var name. +These tests lock in the precedence so a future Speakeasy regen (which +would revert this hand-edit) is caught by CI immediately. +""" + +import os + +import pytest + +from youdotcom.models import Security +from youdotcom.utils.security import get_security_from_env + + +@pytest.fixture(autouse=True) +def clean_env(monkeypatch): + """Strip both env vars before every test so no leaked value contaminates precedence.""" + monkeypatch.delenv("YDC_API_KEY", raising=False) + monkeypatch.delenv("YOU_API_KEY_AUTH", raising=False) + + +def test_ydc_api_key_is_primary_when_set(monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "primary-key") + monkeypatch.setenv("YOU_API_KEY_AUTH", "fallback-key") + + result = get_security_from_env(None, Security) + + assert result is not None + assert result.api_key_auth == "primary-key" + + +def test_you_api_key_auth_fallback_when_ydc_unset(monkeypatch): + monkeypatch.setenv("YOU_API_KEY_AUTH", "fallback-key") + + result = get_security_from_env(None, Security) + + assert result is not None + assert result.api_key_auth == "fallback-key" + + +def test_no_env_returns_none(): + result = get_security_from_env(None, Security) + assert result is None + + +def test_explicit_security_overrides_env(monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "env-key") + + explicit = Security(api_key_auth="explicit-key") + result = get_security_from_env(explicit, Security) + + assert result is not None + assert result.api_key_auth == "explicit-key" diff --git a/uv.lock b/uv.lock index 48cdd68..78a5ad0 100644 --- a/uv.lock +++ b/uv.lock @@ -42,6 +42,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/96/b32bbbb46170a1c8b8b1f28c794202e25cfe743565e9d3469b8eb1e0cc05/astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25", size = 276348, upload-time = "2024-07-20T12:57:40.886Z" }, ] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -127,6 +136,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "isort" version = "5.13.2" @@ -201,6 +219,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + [[package]] name = "platformdirs" version = "4.5.1" @@ -210,6 +237,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -343,6 +379,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pylint" version = "3.2.3" @@ -375,6 +420,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235, upload-time = "2025-03-26T10:06:03.994Z" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + [[package]] name = "tomli" version = "2.3.0" @@ -456,7 +533,7 @@ wheels = [ [[package]] name = "youdotcom" -version = "2.3.0" +version = "2.4.0" source = { editable = "." } dependencies = [ { name = "httpcore" }, @@ -469,13 +546,15 @@ dev = [ { name = "mypy" }, { name = "pylint" }, { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, ] [package.metadata] requires-dist = [ { name = "httpcore", specifier = ">=1.0.9" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "pydantic", specifier = ">=2.11.2" }, + { name = "pydantic", specifier = ">=2.11.2,<2.13" }, ] [package.metadata.requires-dev] @@ -483,4 +562,6 @@ dev = [ { name = "mypy", specifier = "==1.15.0" }, { name = "pylint", specifier = "==3.2.3" }, { name = "pyright", specifier = "==1.1.398" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.24.0" }, ]