From 75115cdff2719e405780315ce4a312145614fa39 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 10:47:34 -0700 Subject: [PATCH 01/20] Release v2.6.0: Answer API, Speakeasy removal, flattened API surface ### Added - Answer API: you.answer() / you.answer_async() for POST /v1/answer. Returns synthesized markdown answers with inline citations and web results. - PaymentRequiredResponseError: first-class 402 error class for the answer API (UpgradeRequiredResponse schema). ### Changed - Flattened API surface: sub-SDK chains (you.search.unified(), you.contents.generate()) are deprecated with DeprecationWarning and delegate to new direct methods (you.search(), you.contents()). Plain strings accepted instead of enum imports for country, language, safesearch, livecrawl, freshness. - Removed all Speakeasy generated code and disclaimers. SDK is now fully hand-maintained. - Removed YDCUserAgentOverrideHook (no-op after Speakeasy removal). __user_agent__ derived from __version__ at runtime. - search_helpers module merged into you.search() direct method. - 422/500 error data models expanded with optional detail/errors fields (backward compatible). - Dev dependencies updated: mypy >=2.3.0, pylint >=4.0.0, pytest >=9.0.0, pytest-asyncio >=1.0.0. - CI: replaced pylint/pyright with mypy, excluded live tests from CI. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../skills/generate-sdk-and-open-pr/SKILL.md | 516 ++---------- .github/workflows/test.yml | 9 +- CHANGELOG.md | 37 +- CONTRIBUTING.md | 26 +- MIGRATION.md | 115 ++- README.md | 161 ++-- USAGE.md | 4 +- docs/errors/agentruns400responseerror.md | 10 - docs/errors/agentruns401responseerror.md | 10 - docs/errors/agentruns422responseerror.md | 8 - docs/errors/internalservererrorresponse.md | 11 +- docs/errors/paymentrequiredresponseerror.md | 16 + .../unprocessableentityresponseerror.md | 13 +- docs/models/advancedagentrunsrequest.md | 13 - docs/models/agentrunsbatchresponse.md | 11 - docs/models/agentrunsresponseoutput.md | 12 - .../agentrunsresponsewebsearchresult.md | 16 - docs/models/agentrunsstreamingresponse.md | 12 - docs/models/agentsrunsrequest.md | 25 - docs/models/agentsrunsresponse.md | 17 - docs/models/answercitation.md | 11 + docs/models/answerrequestbody.md | 16 + docs/models/answerresponse.md | 19 + docs/models/answersearchresult.md | 13 + docs/models/computetool.md | 8 - docs/models/contentsmetadata.md | 2 +- docs/models/customagentrunsrequest.md | 10 - docs/models/data.md | 47 -- docs/models/expressagentrunsrequest.md | 11 - docs/models/financeresearcheffort.md | 4 +- docs/models/input.md | 9 - docs/models/reportverbosity.md | 19 - docs/models/researchtool.md | 10 - docs/models/responsecreated.md | 11 - docs/models/responsedone.md | 10 - docs/models/responsedoneresponse.md | 9 - docs/models/responseoutputcontentfull.md | 10 - .../responseoutputcontentfullresponse.md | 10 - docs/models/responseoutputitemadded.md | 12 - .../models/responseoutputitemaddedresponse.md | 8 - docs/models/responseoutputitemdone.md | 10 - docs/models/responseoutputitemdoneresponse.md | 8 - docs/models/responseoutputtextdelta.md | 10 - .../models/responseoutputtextdeltaresponse.md | 10 - docs/models/responsestarting.md | 11 - docs/models/role.md | 18 - docs/models/searcheffort.md | 23 - docs/models/tool.md | 17 - docs/models/type.md | 21 - docs/models/verbosity.md | 19 - docs/models/websearchtool.md | 8 - docs/models/workflowconfig.md | 10 - docs/sdks/answer/README.md | 66 ++ docs/sdks/contentssdk/README.md | 9 +- docs/sdks/runs/README.md | 201 ----- docs/sdks/search/README.md | 7 + docs/sdks/you/README.md | 78 +- examples/api-example-calls.py | 183 +---- overlays/python_overlay.yaml | 121 --- pyproject.toml | 12 +- src/youdotcom/__init__.py | 4 +- src/youdotcom/_hooks/__init__.py | 3 +- src/youdotcom/_hooks/registration.py | 45 -- src/youdotcom/_hooks/sdkhooks.py | 4 +- src/youdotcom/_hooks/types.py | 2 +- src/youdotcom/_shims.py | 255 ++++++ src/youdotcom/_version.py | 7 +- src/youdotcom/agents.py | 20 - src/youdotcom/basesdk.py | 21 +- src/youdotcom/contents_sdk.py | 239 ------ src/youdotcom/errors/__init__.py | 34 +- .../errors/agentruns400response_error.py | 29 - .../errors/agentruns401response_error.py | 29 - .../errors/agentruns422response_error.py | 30 - src/youdotcom/errors/contentsop.py | 2 +- src/youdotcom/errors/finance_researchop.py | 2 +- .../errors/forbidden_response_error.py | 2 +- src/youdotcom/errors/getresearchtaskop.py | 2 +- .../errors/internalservererror_response.py | 16 +- src/youdotcom/errors/no_response_error.py | 2 +- .../errors/paymentrequired_response_error.py | 43 + src/youdotcom/errors/researchop.py | 2 +- .../errors/responsevalidationerror.py | 2 +- src/youdotcom/errors/streamresearchtaskop.py | 2 +- .../errors/unauthorized_response_error.py | 2 +- .../unprocessableentity_response_error.py | 23 +- src/youdotcom/errors/youdefaulterror.py | 2 +- src/youdotcom/errors/youerror.py | 2 +- src/youdotcom/httpclient.py | 2 +- src/youdotcom/models/__init__.py | 235 +----- .../models/advancedagentrunsrequest.py | 109 --- .../models/agentruns422response_error.py | 29 - .../models/agentrunsbatchresponse.py | 74 -- .../models/agentrunsresponseoutput.py | 78 -- .../agentrunsresponsewebsearchresult.py | 79 -- .../models/agentrunsstreamingresponse.py | 76 -- src/youdotcom/models/agentsrunsop.py | 73 -- src/youdotcom/models/answercitation.py | 13 + src/youdotcom/models/answerrequestbody.py | 50 ++ src/youdotcom/models/answerresponse.py | 25 + src/youdotcom/models/answersearchresult.py | 19 + src/youdotcom/models/computetool.py | 28 - src/youdotcom/models/contents.py | 2 +- src/youdotcom/models/contentsformats.py | 2 +- src/youdotcom/models/contentsmetadata.py | 2 +- src/youdotcom/models/contentsop.py | 2 +- src/youdotcom/models/country.py | 2 +- .../models/customagentrunsrequest.py | 43 - .../models/expressagentrunsrequest.py | 61 -- src/youdotcom/models/finance_researchop.py | 2 +- src/youdotcom/models/financeresearcheffort.py | 5 - src/youdotcom/models/freshness.py | 2 +- src/youdotcom/models/freshnessvalue.py | 2 +- src/youdotcom/models/getresearchtaskop.py | 2 +- src/youdotcom/models/language.py | 2 +- src/youdotcom/models/livecrawl.py | 2 +- src/youdotcom/models/livecrawlformats.py | 2 +- src/youdotcom/models/newsresult.py | 2 +- src/youdotcom/models/reportverbosity.py | 11 - src/youdotcom/models/researcheffort.py | 3 - src/youdotcom/models/researchop.py | 2 +- src/youdotcom/models/researchresponse.py | 2 +- .../models/researchtaskstreamevent.py | 2 +- src/youdotcom/models/researchtool.py | 46 -- src/youdotcom/models/response_created.py | 36 - src/youdotcom/models/response_done.py | 49 -- .../models/response_output_content_full.py | 65 -- .../models/response_output_item_added.py | 49 -- .../models/response_output_item_done.py | 43 - .../models/response_output_text_delta.py | 60 -- src/youdotcom/models/response_starting.py | 36 - src/youdotcom/models/safesearch.py | 2 +- src/youdotcom/models/searcheffort.py | 16 - src/youdotcom/models/searchmetadata.py | 2 +- src/youdotcom/models/searchop.py | 2 +- src/youdotcom/models/searchpostop.py | 8 - src/youdotcom/models/searchrequestbody.py | 2 +- src/youdotcom/models/searchresponse.py | 2 +- src/youdotcom/models/security.py | 2 +- src/youdotcom/models/streamresearchtaskop.py | 2 +- src/youdotcom/models/taskdetail.py | 2 +- src/youdotcom/models/taskresponse.py | 2 +- src/youdotcom/models/verbosity.py | 11 - src/youdotcom/models/webresult.py | 2 +- src/youdotcom/models/websearchtool.py | 28 - src/youdotcom/research_helpers.py | 3 +- src/youdotcom/runs.py | 293 ------- src/youdotcom/sdk.py | 736 ++++++++++++++---- src/youdotcom/sdkconfiguration.py | 4 +- src/youdotcom/search.py | 325 -------- src/youdotcom/types/__init__.py | 2 +- src/youdotcom/types/base64fileinput.py | 2 +- src/youdotcom/types/basemodel.py | 2 +- src/youdotcom/utils/__init__.py | 2 +- src/youdotcom/utils/annotations.py | 2 +- src/youdotcom/utils/datetimes.py | 2 +- src/youdotcom/utils/dynamic_imports.py | 2 +- src/youdotcom/utils/enums.py | 2 +- src/youdotcom/utils/eventstreaming.py | 2 +- src/youdotcom/utils/forms.py | 2 +- src/youdotcom/utils/headers.py | 2 +- src/youdotcom/utils/logger.py | 2 +- src/youdotcom/utils/metadata.py | 2 +- src/youdotcom/utils/queryparams.py | 2 +- src/youdotcom/utils/requestbodies.py | 2 +- src/youdotcom/utils/retries.py | 2 +- src/youdotcom/utils/security.py | 6 +- src/youdotcom/utils/serializers.py | 2 +- .../utils/unmarshal_json_response.py | 2 +- src/youdotcom/utils/url.py | 2 +- src/youdotcom/utils/values.py | 2 +- tests/PERFORMANCE_TESTING.md | 19 +- tests/README.md | 32 +- tests/__init__.py | 2 - tests/mockserver/README.md | 2 +- .../internal/handler/assert/contenttype.go | 1 - .../internal/handler/assert/header.go | 1 - .../internal/handler/assert/parameter.go | 1 - .../internal/handler/assert/pointer.go | 1 - .../internal/handler/assert/security.go | 1 - tests/mockserver/internal/handler/doc.go | 1 - .../internal/handler/generated_handler.go | 1 - .../internal/handler/generated_handlers.go | 4 +- .../internal/handler/pathgetv1research.go | 4 +- .../handler/pathgetv1researchstream.go | 4 +- .../internal/handler/pathgetv1search.go | 109 --- .../internal/handler/pathpostv1agentsruns.go | 131 ---- .../internal/handler/pathpostv1contents.go | 5 +- .../handler/pathpostv1financeresearch.go | 4 +- .../internal/handler/pathpostv1research.go | 4 +- .../internal/handler/pathpostv1search.go | 56 ++ .../internal/handler/values/files.go | 1 - tests/mockserver/internal/logging/doc.go | 1 - tests/mockserver/internal/logging/formats.go | 1 - .../mockserver/internal/logging/http_file.go | 1 - .../internal/logging/http_logger.go | 1 - tests/mockserver/internal/logging/levels.go | 1 - tests/mockserver/internal/logging/logger.go | 1 - .../internal/logging/oas_operation.go | 1 - .../internal/logging/oas_operation_call.go | 1 - .../sdk/models/components/agenttype.go | 35 - .../sdk/models/components/chatanswerfull.go | 72 -- .../models/components/computeresultsfull.go | 42 - .../sdk/models/components/computetool.go | 27 - .../internal/sdk/models/components/country.go | 1 - .../sdk/models/components/freshness.go | 1 - .../sdk/models/components/fullresponse.go | 182 ----- .../sdk/models/components/genericfull.go | 38 - .../sdk/models/components/httpmetadata.go | 1 - .../sdk/models/components/livecrawl.go | 1 - .../sdk/models/components/livecrawlformats.go | 1 - .../models/components/livecrawlresultsfull.go | 42 - .../models/components/researchresultsfull.go | 80 -- .../sdk/models/components/researchtool.go | 173 ---- .../sdk/models/components/safesearch.go | 1 - .../sdk/models/components/searcheffortenum.go | 41 - .../sdk/models/components/security.go | 1 - .../internal/sdk/models/components/tool.go | 113 --- .../internal/sdk/models/components/trigger.go | 35 - .../sdk/models/components/verbosity.go | 38 - .../models/components/websearchresultsfull.go | 80 -- .../sdk/models/components/websearchtool.go | 36 - .../sdk/models/operations/getv1search.go | 640 --------------- .../sdk/models/operations/postv1agentsruns.go | 591 -------------- .../sdk/models/operations/postv1contents.go | 1 - .../sdk/models/sdkerrors/getv1search.go | 48 -- .../sdk/models/sdkerrors/postv1agentsruns.go | 48 -- .../sdk/models/sdkerrors/postv1contents.go | 1 - tests/mockserver/internal/sdk/types/bigint.go | 1 - tests/mockserver/internal/sdk/types/date.go | 1 - .../mockserver/internal/sdk/types/datetime.go | 1 - .../mockserver/internal/sdk/types/decimal.go | 1 - .../internal/sdk/types/jsonl/jsonl.go | 1 - .../mockserver/internal/sdk/types/pointers.go | 1 - .../internal/sdk/types/stream/stream.go | 1 - tests/mockserver/internal/sdk/utils/json.go | 1 - .../mockserver/internal/sdk/utils/reflect.go | 1 - tests/mockserver/internal/sdk/utils/sort.go | 1 - tests/mockserver/internal/server/doc.go | 1 - .../internal/server/generated_handlers.go | 1 - .../internal/server/internal_handlers.go | 1 - tests/mockserver/internal/server/server.go | 1 - .../internal/server/server_option.go | 1 - .../internal/tracking/requesttracker.go | 1 - tests/mockserver/main.go | 1 - tests/test_answer.py | 282 +++++++ tests/test_client.py | 4 +- tests/test_contents.py | 24 +- tests/test_direct_methods.py | 152 ++++ tests/test_live.py | 199 ++--- tests/test_performance.py | 460 ++--------- tests/test_research.py | 22 +- tests/test_research_helpers.py | 20 +- tests/test_runs.py | 256 ------ tests/test_search.py | 318 +++----- tests/test_shims.py | 94 +++ tests/test_user_agent_hook.py | 59 -- uv.lock | 244 +++++- 258 files changed, 2787 insertions(+), 8004 deletions(-) delete mode 100644 docs/errors/agentruns400responseerror.md delete mode 100644 docs/errors/agentruns401responseerror.md delete mode 100644 docs/errors/agentruns422responseerror.md create mode 100644 docs/errors/paymentrequiredresponseerror.md delete mode 100644 docs/models/advancedagentrunsrequest.md delete mode 100644 docs/models/agentrunsbatchresponse.md delete mode 100644 docs/models/agentrunsresponseoutput.md delete mode 100644 docs/models/agentrunsresponsewebsearchresult.md delete mode 100644 docs/models/agentrunsstreamingresponse.md delete mode 100644 docs/models/agentsrunsrequest.md delete mode 100644 docs/models/agentsrunsresponse.md create mode 100644 docs/models/answercitation.md create mode 100644 docs/models/answerrequestbody.md create mode 100644 docs/models/answerresponse.md create mode 100644 docs/models/answersearchresult.md delete mode 100644 docs/models/computetool.md delete mode 100644 docs/models/customagentrunsrequest.md delete mode 100644 docs/models/data.md delete mode 100644 docs/models/expressagentrunsrequest.md delete mode 100644 docs/models/input.md delete mode 100644 docs/models/reportverbosity.md delete mode 100644 docs/models/researchtool.md delete mode 100644 docs/models/responsecreated.md delete mode 100644 docs/models/responsedone.md delete mode 100644 docs/models/responsedoneresponse.md delete mode 100644 docs/models/responseoutputcontentfull.md delete mode 100644 docs/models/responseoutputcontentfullresponse.md delete mode 100644 docs/models/responseoutputitemadded.md delete mode 100644 docs/models/responseoutputitemaddedresponse.md delete mode 100644 docs/models/responseoutputitemdone.md delete mode 100644 docs/models/responseoutputitemdoneresponse.md delete mode 100644 docs/models/responseoutputtextdelta.md delete mode 100644 docs/models/responseoutputtextdeltaresponse.md delete mode 100644 docs/models/responsestarting.md delete mode 100644 docs/models/role.md delete mode 100644 docs/models/searcheffort.md delete mode 100644 docs/models/tool.md delete mode 100644 docs/models/type.md delete mode 100644 docs/models/verbosity.md delete mode 100644 docs/models/websearchtool.md delete mode 100644 docs/models/workflowconfig.md create mode 100644 docs/sdks/answer/README.md delete mode 100644 docs/sdks/runs/README.md delete mode 100644 overlays/python_overlay.yaml delete mode 100644 src/youdotcom/_hooks/registration.py create mode 100644 src/youdotcom/_shims.py delete mode 100644 src/youdotcom/agents.py delete mode 100644 src/youdotcom/contents_sdk.py delete mode 100644 src/youdotcom/errors/agentruns400response_error.py delete mode 100644 src/youdotcom/errors/agentruns401response_error.py delete mode 100644 src/youdotcom/errors/agentruns422response_error.py create mode 100644 src/youdotcom/errors/paymentrequired_response_error.py delete mode 100644 src/youdotcom/models/advancedagentrunsrequest.py delete mode 100644 src/youdotcom/models/agentruns422response_error.py delete mode 100644 src/youdotcom/models/agentrunsbatchresponse.py delete mode 100644 src/youdotcom/models/agentrunsresponseoutput.py delete mode 100644 src/youdotcom/models/agentrunsresponsewebsearchresult.py delete mode 100644 src/youdotcom/models/agentrunsstreamingresponse.py delete mode 100644 src/youdotcom/models/agentsrunsop.py create mode 100644 src/youdotcom/models/answercitation.py create mode 100644 src/youdotcom/models/answerrequestbody.py create mode 100644 src/youdotcom/models/answerresponse.py create mode 100644 src/youdotcom/models/answersearchresult.py delete mode 100644 src/youdotcom/models/computetool.py delete mode 100644 src/youdotcom/models/customagentrunsrequest.py delete mode 100644 src/youdotcom/models/expressagentrunsrequest.py delete mode 100644 src/youdotcom/models/reportverbosity.py delete mode 100644 src/youdotcom/models/researchtool.py delete mode 100644 src/youdotcom/models/response_created.py delete mode 100644 src/youdotcom/models/response_done.py delete mode 100644 src/youdotcom/models/response_output_content_full.py delete mode 100644 src/youdotcom/models/response_output_item_added.py delete mode 100644 src/youdotcom/models/response_output_item_done.py delete mode 100644 src/youdotcom/models/response_output_text_delta.py delete mode 100644 src/youdotcom/models/response_starting.py delete mode 100644 src/youdotcom/models/searcheffort.py delete mode 100644 src/youdotcom/models/searchpostop.py delete mode 100644 src/youdotcom/models/verbosity.py delete mode 100644 src/youdotcom/models/websearchtool.py delete mode 100644 src/youdotcom/runs.py delete mode 100644 src/youdotcom/search.py delete mode 100644 tests/mockserver/internal/handler/pathgetv1search.go delete mode 100644 tests/mockserver/internal/handler/pathpostv1agentsruns.go create mode 100644 tests/mockserver/internal/handler/pathpostv1search.go delete mode 100644 tests/mockserver/internal/sdk/models/components/agenttype.go delete mode 100644 tests/mockserver/internal/sdk/models/components/chatanswerfull.go delete mode 100644 tests/mockserver/internal/sdk/models/components/computeresultsfull.go delete mode 100644 tests/mockserver/internal/sdk/models/components/computetool.go delete mode 100644 tests/mockserver/internal/sdk/models/components/fullresponse.go delete mode 100644 tests/mockserver/internal/sdk/models/components/genericfull.go delete mode 100644 tests/mockserver/internal/sdk/models/components/livecrawlresultsfull.go delete mode 100644 tests/mockserver/internal/sdk/models/components/researchresultsfull.go delete mode 100644 tests/mockserver/internal/sdk/models/components/researchtool.go delete mode 100644 tests/mockserver/internal/sdk/models/components/searcheffortenum.go delete mode 100644 tests/mockserver/internal/sdk/models/components/tool.go delete mode 100644 tests/mockserver/internal/sdk/models/components/trigger.go delete mode 100644 tests/mockserver/internal/sdk/models/components/verbosity.go delete mode 100644 tests/mockserver/internal/sdk/models/components/websearchresultsfull.go delete mode 100644 tests/mockserver/internal/sdk/models/components/websearchtool.go delete mode 100644 tests/mockserver/internal/sdk/models/operations/getv1search.go delete mode 100644 tests/mockserver/internal/sdk/models/operations/postv1agentsruns.go delete mode 100644 tests/mockserver/internal/sdk/models/sdkerrors/getv1search.go delete mode 100644 tests/mockserver/internal/sdk/models/sdkerrors/postv1agentsruns.go create mode 100644 tests/test_answer.py create mode 100644 tests/test_direct_methods.py delete mode 100644 tests/test_runs.py create mode 100644 tests/test_shims.py delete mode 100644 tests/test_user_agent_hook.py diff --git a/.agents/skills/generate-sdk-and-open-pr/SKILL.md b/.agents/skills/generate-sdk-and-open-pr/SKILL.md index ff95852..0dd146a 100644 --- a/.agents/skills/generate-sdk-and-open-pr/SKILL.md +++ b/.agents/skills/generate-sdk-and-open-pr/SKILL.md @@ -1,10 +1,10 @@ --- name: generate-sdk-and-open-pr -description: Generate the Speakeasy SDK for a new version and open a release PR +description: Release a new version of the youdotcom Python SDK and open a PR allowed-tools: Bash, Read, Write, Edit, Glob, Grep metadata: author: youdotcom-oss - version: "1.0.0" + version: "2.0.0" category: release keywords: release, version, publish, pypi --- @@ -13,178 +13,51 @@ metadata: Release a new version of the `youdotcom` Python SDK to PyPI and GitHub. -## Step 1: Verify OpenAPI specs +The SDK is hand-maintained (no longer generated by Speakeasy). This skill +covers version bumping, changelog updates, testing, and opening a release PR. -Speakeasy generates the SDK from OpenAPI specs defined in `.speakeasy/workflow.yaml`. The current source specs are: - -- `https://you.com/specs/openapi_unified_agents.yaml` -- `https://you.com/specs/openapi_search_v1.yaml` -- `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`. - -### 1a. Ask the user about spec sources - -Use `AskUserQuestion` to ask: - -``` -The SDK is generated from these OpenAPI specs: - -1. https://you.com/specs/openapi_unified_agents.yaml -2. https://you.com/specs/openapi_search_v1.yaml -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? -``` - -Options: -- **Use existing specs** (the remote URLs already have the changes) -- **Use custom specs** (user will provide spec content or file paths) - -### 1b. If custom specs - -If the user provides custom specs: - -1. Ask which spec(s) they want to replace and get the new content or file path -2. Update the `inputs` locations in `.speakeasy/workflow.yaml` to point to the custom spec files (e.g. change the remote URL to a local path) -3. **IMPORTANT**: Do NOT commit changes to `.speakeasy/workflow.yaml`. These are temporary overrides for generation only. Remind the user that these changes should be reverted or excluded from the release commit. - -If using existing specs, move on to step 2. - -## Step 2: Check current versions and fetch latest changes - -Before anything else, gather the current state of the world. - -### 1a. Fetch all remote changes +## Step 1: Check current versions ```bash git fetch --all --tags -``` - -### 1b. Check the latest GitHub release - -```bash gh release list --repo youdotcom-oss/youdotcom-python-sdk --limit 1 -``` - -### 1c. Check the latest version on PyPI - -```bash curl -s https://pypi.org/pypi/youdotcom/json | python3 -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" ``` -### 1d. Check the local version - Read the version from `pyproject.toml` (line 3) and `src/youdotcom/_version.py`. -### 1e. Report findings - -Present a summary to the user: -- **GitHub release**: latest tag/release name -- **PyPI version**: latest published version -- **Local version**: version in pyproject.toml and _version.py -- **Unreleased commits**: `git log ..HEAD --oneline` +Present a summary: +- GitHub release: latest tag +- PyPI version: latest published +- Local version: in pyproject.toml and _version.py +- Unreleased commits: `git log ..HEAD --oneline` -If any versions are out of sync, warn the user before proceeding. +## Step 2: Confirm the next version -## Step 3: Confirm the next version with the user - -Analyze the unreleased commits from step 1e to determine the appropriate semver bump: -- **patch** (X.Y.Z+1): bug fixes, dependency updates, docs changes only +Analyze unreleased commits to determine the semver bump: +- **patch** (X.Y.Z+1): bug fixes, dependency updates, docs - **minor** (X.Y+1.0): new features, non-breaking additions -- **major** (X+1.0.0): breaking API changes, removed endpoints, changed response types - -Use the highest version found across PyPI, GitHub, and local as the base for the bump. - -Present the version summary and suggestion to the user using `AskUserQuestion`: - -``` -Latest PyPI version: X.Y.Z -Latest GitHub version: X.Y.Z -Local version: X.Y.Z - -Suggested version: X.Y.Z based on [brief reasoning from commit analysis, e.g. "new endpoints added in 3 commits" or "bug fixes only"] - -Proceed with update to version X.Y.Z? -``` - -Offer the suggested version as the recommended option, plus the other two semver bump levels as alternatives (e.g. if suggesting minor, also offer patch and major). Let the user pick or provide a custom version. - -Do NOT proceed until the user confirms. - -## Step 4: Generate the SDK and open a release PR +- **major** (X+1.0.0): breaking API changes, removed endpoints -### 4a. Confirm SDK generation +Use `AskUserQuestion` to confirm the version with the user. Do NOT proceed until confirmed. -Use `AskUserQuestion` to confirm: - -``` -Ready to run Speakeasy SDK generation for version X.Y.Z. This will regenerate the SDK source code from the OpenAPI specs. - -Proceed with generation? -``` - -Options: -- **Yes, generate** (recommended) -- **No, cancel** - -Do NOT proceed if the user cancels. - -### 4b. Bump version via Speakeasy - -Use `speakeasy bump` to set the version in `.speakeasy/gen.yaml`. This is the canonical way to update the Speakeasy target version. - -```bash -speakeasy bump -v X.Y.Z -t you -``` - -This updates `python.version` in `.speakeasy/gen.yaml` to the confirmed version. - -### 4c. Run Speakeasy generation - -```bash -speakeasy run -``` - -This will: -- Fetch the OpenAPI specs (remote URLs or local overrides from step 1) -- Apply the overlay from `overlays/python_overlay.yaml` -- Regenerate all SDK source files under `src/` -- Regenerate `USAGE.md` and auto-generated sections in `README.md` (the `` blocks) -- Update `.speakeasy/out.openapi.yaml` - -Wait for the command to complete and check for errors. If it fails, report the error to the user and stop. - -### 4d. Revert temporary workflow changes - -If custom specs were used in step 1, revert `.speakeasy/workflow.yaml` back to the original remote URLs: - -```bash -git checkout -- .speakeasy/workflow.yaml -``` - -### 4e. Create a release branch +## Step 3: Create a release branch ```bash git checkout -b release/X.Y.Z ``` -### 4f. Update version in all locations +## Step 4: Update version -Update the version string in these files (if not already updated by Speakeasy): +Update the version string in: - `pyproject.toml` — `version = "X.Y.Z"` -- `src/youdotcom/_version.py` — `__version__: str = "X.Y.Z"` and the `__user_agent__` string +- `src/youdotcom/_version.py` — `__version__: str = "X.Y.Z"` (`__user_agent__` is derived automatically) -### 4g. Update markdown documentation +## Step 5: Update documentation -#### CHANGELOG.md -Add a new section at the top (below the header), following the existing Keep a Changelog format: +### CHANGELOG.md + +Add a new section at the top, following Keep a Changelog format: ```markdown ## [X.Y.Z] - YYYY-MM-DD @@ -199,357 +72,90 @@ Add a new section at the top (below the header), following the existing Keep a C - ... ``` -Analyze the diff between the previous version and the newly generated code to determine what changed. Categorize changes into Added/Changed/Removed sections. Include code examples for significant API changes. - -#### MIGRATION.md -Only update if there are breaking changes (major version bump). Add a new migration section at the top with before/after code examples, following the existing style. - -#### USAGE.md -This file is auto-generated by Speakeasy (`` blocks). Verify it was updated by the generation step. If the examples are outdated or incorrect, update them. - -#### README.md -The `` blocks are auto-generated by Speakeasy. Verify they were updated. Do NOT modify content outside these blocks unless necessary. +Analyze `git diff ..HEAD` to determine what changed. -#### docs/ folder -The `docs/` folder contains auto-generated model and SDK documentation. These are updated by Speakeasy generation. Verify they look correct but do not manually edit them. +### MIGRATION.md -### 4h. Update and validate tests +Only update if there are breaking changes. Add a migration section with before/after code examples. -After generation and doc updates, ensure the test suite is compatible with the new SDK code. +### README.md -#### Test structure +Update the error class count, method count, and any examples if the API surface changed. -- **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 `YDC_API_KEY` env var. -- **Client tests** (`tests/test_client.py`): Test HTTP client setup helpers. +## Step 6: Run tests -#### 4h-1. Update tests for new/changed APIs - -Review the generated diff from step 4c. If Speakeasy added, removed, or changed any models, endpoints, or parameters: - -1. Update unit tests to reflect the new request/response shapes -2. Update integration tests (`test_live.py`) if endpoints or model imports changed -3. Add new test cases for any new endpoints or features - -#### 4h-2. Run unit tests +### Start the mock server ```bash -pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v +cd tests/mockserver && go build -o mockserver . && ./mockserver & +sleep 1 +cd ../.. ``` -If tests fail, fix the test code (or SDK issues if applicable) and re-run. - -#### 4h-3. Run integration tests (if API key is available) +### Unit tests ```bash -pytest tests/test_live.py -v +.venv/bin/python -m pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v ``` -If `YDC_API_KEY` is not set, skip this step and note it in the PR description. - -#### 4h-4. Validate tests line by line - -After all tests pass, read through every changed test file line by line. Check for: -- Incorrect model imports that no longer exist -- Hardcoded values that should have been updated for the new version -- Missing assertions for new response fields -- Dead test cases for removed endpoints -- Inconsistencies between test expectations and the actual generated SDK code - -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. 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: +### Performance tests ```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 +.venv/bin/python -m pytest tests/test_performance.py -v ``` -Verify only `security.py` retains `YOU_API_KEY_AUTH` (the fallback): +### Type checking ```bash -grep -rl "YOU_API_KEY_AUTH" --include="*.py" --include="*.md" . | grep -v __pycache__ | grep -v build/ | grep -v examples/ | grep -v tests/test_security_env.py -# Expected runtime-source hit: ./src/youdotcom/utils/security.py -# Docs and CHANGELOG/MIGRATION/USAGE may also mention YOU_API_KEY_AUTH by -# name (as the documented 2.3.x fallback being kept). tests/test_security_env.py -# is excluded because it intentionally references both env vars to lock -# in the fallback precedence. -``` - -#### 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" -) +.venv/bin/mypy src/youdotcom/ ``` -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: +### Live tests (if API key available) ```bash -pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v +YDC_API_KEY=... .venv/bin/python -m pytest tests/test_live.py -v ``` -#### 4i-7. Run full validation suite - -After all post-generation fixes are applied, run the complete validation: +### Stop the mock server ```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: +All tests and mypy must pass before committing. -- `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`: +## Step 7: Verify server URLs ```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. +# Default server (answer, research, finance_research) = api.you.com +grep "api.you.com" src/youdotcom/sdkconfiguration.py +# Search/contents servers = ydc-index.io +grep "ydc-index.io" src/youdotcom/models/searchop.py src/youdotcom/models/contentsop.py ``` -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. +Search and contents methods use `SEARCH_OP_SERVERS[0]` / `CONTENTS_OP_SERVERS[0]` explicitly. Answer, research, and finance_research use the default `SERVERS[0]`. -Also scan for the `RetryConfig(...)` positional-after-kwargs regression that Speakeasy can produce when `search_post` is the lead example operation: +## Step 8: Verify hand-maintained files -```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) +These files are hand-maintained and must not be lost: +- `src/youdotcom/_shims.py` — backward-compat shims (`SearchShim`, `ContentsShim`) +- `src/youdotcom/research_helpers.py` — background-mode research helpers +- `src/youdotcom/errors/paymentrequired_response_error.py` — 402 error class -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: +## Step 9: Commit, push, and open PR ```bash git add -A -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. - -### 4k. Push and open a PR - -```bash +git commit -m "Release vX.Y.Z: " git push -u origin release/X.Y.Z ``` -Open a PR against `main` using `gh`: +Open a PR against `main`: ```bash -gh pr create --title "Python SDK X.Y.Z" --body "$(cat <<'EOF' -## Summary -- Release version X.Y.Z of the `youdotcom` Python SDK -- [Brief description of what changed based on changelog entries] - -## Changes -[List key changes from the changelog] - -## Checklist -- [ ] Speakeasy generation ran successfully -- [ ] Version updated in pyproject.toml, gen.yaml, and _version.py -- [ ] CHANGELOG.md updated -- [ ] MIGRATION.md updated (if breaking changes) -- [ ] README.md and USAGE.md verified -- [ ] Tests updated and passing -EOF -)" +gh pr create --base main --head release/X.Y.Z \ + --title "Release vX.Y.Z: " \ + --body "" ``` -Report the PR URL to the user when done. +Report the PR URL to the user. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c0689c..3ca03e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,8 +28,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e ".[dev]" - pip install pytest pytest-asyncio + pip install -e . + pip install "mypy>=2.3.0" "pytest>=9.0.0" "pytest-asyncio>=1.0.0" - name: Build and start mock server working-directory: tests/mockserver @@ -40,4 +40,7 @@ jobs: curl -sf http://localhost:18080/ || echo "mock server ready" - name: Run tests - run: pytest tests/ -v --tb=short -x + run: pytest tests/ -v --tb=short -x --ignore=tests/test_live.py + + - name: Run mypy + run: mypy src/youdotcom/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 75363e7..8b06cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,40 @@ 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.6.0] - 2026-08-04 + +### Deprecations + +- **Sub-SDK access patterns deprecated**: The sub-SDK layer added unnecessary indirection — `you.search.unified()` went through extra layers before reaching the HTTP client. The new direct methods (`you.search()`, `you.contents()`) collapse this chain into a single call on `You`, with simpler signatures that accept plain strings instead of enum imports. The old patterns still work but emit `DeprecationWarning` and delegate to the new methods. Migrate at your convenience: + +| Old (deprecated) | New | +|---------------|-----| +| `you.search.unified(query=...)` | `you.search(query=...)` | +| `you.search.unified_async(query=...)` | `you.search_async(query=...)` | +| `you.contents.generate(urls=...)` | `you.contents(urls=...)` | +| `you.contents.generate_async(urls=...)` | `you.contents_async(urls=...)` | + +- **`search_helpers` module removed**: The standalone `search_helpers.search()` function has been merged into `you.search()`. Import `from youdotcom import You` and call `you.search(query=...)` directly. +- **Agents API removed**: The `you.agents()` / `you.agents_async()` direct methods and the `you.agents.runs` sub-SDK shim have been removed. All Agents API model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, `AgentRunsResponseOutput`, `AgentRunsStreamingResponse`, `AgentRunsResponseWebSearchResult`, `ComputeTool`, `WebSearchTool`, `ResearchTool`, `ReportVerbosity`, `SearchEffort`, `Verbosity`, and streaming event models) and error classes (`AgentRuns400ResponseError`, `AgentRuns401ResponseError`, `AgentRuns422ResponseError`) have been deleted from `youdotcom.models` and `youdotcom.errors`. + +### Added + +- **Answer API**: New direct method `you.answer()` / `you.answer_async()` for `POST /v1/answer`. Returns a synthesized markdown answer with inline citations (`[[1, 2]]`), a citations array (source URLs + supporting excerpts), and web results. Accepts `query` (required), `freshness`, `country`, `language`, `include_domains`, `exclude_domains`, `boost_domains`. Requires an API key. Country and language accept plain strings (e.g. `"us"`, `"en"`) and are normalized to uppercase automatically. +- **`PaymentRequiredResponseError`**: New first-class error class for HTTP 402 responses, with data model `PaymentRequiredResponseErrorData` (`error`, `message`, `upgrade_url`, `limit`, `used`, `period`, `reset_at`). Used by the answer 402 handler. + +### Changed + +- **Search/Contents host**: `SEARCH_OP_SERVERS` and `CONTENTS_OP_SERVERS` point to `https://ydc-index.io`. The default server URL (used by answer, research, finance research) is `https://api.you.com`. `you.search()` and `you.contents()` now resolve to their own server constants instead of the default. `you.search()` targets `POST /v1/search` (keyed, requires an API key). +- **422 error data model**: `UnprocessableEntityResponseErrorData` now includes optional `detail` (FastAPI validation array) and `errors` (JSON:API array) fields in addition to the existing `error` field. All three 422 response shapes deserialize without crashing. Backward compatible — existing code accessing `.error` still works. +- **500 error data model**: `InternalServerErrorResponseData` now includes an optional `errors` field for JSON:API format 500 responses. Backward compatible. +- **No longer generated by Speakeasy**: Removed all "Code generated by Speakeasy — DO NOT EDIT" disclaimers and the Speakeasy badge from the README. The SDK is now hand-maintained. +- **Removed `YDCUserAgentOverrideHook`**: The hook existed to rewrite Speakeasy's default UA (`speakeasy-sdk/python ...`) to `youdotcom-python-sdk/{version}`. Now that `__user_agent__` is already `youdotcom-python-sdk/{version}`, `BaseSDK._build_request` sets it directly — the hook was a no-op. Integrations that need a custom UA still just set `client.sdk_configuration.user_agent`. +- **`__user_agent__` derived from resolved `__version__`**: The user-agent string is now built from the package's resolved version at runtime rather than a hardcoded value. +- **Dead code removal**: Deleted `_hooks/registration.py` (no-op `init_hooks`), `models/answerop.py` (unused `ANSWER_OP_SERVERS`), unused `importlib` and `TYPE_CHECKING` imports from `sdk.py`. Deleted all remaining agent model/error classes and their doc files. Deleted `overlays/python_overlay.yaml` (Speakeasy overlay, no longer used). +- **`search()` server_url fix**: `search()` and `search_async()` now use `self._get_url(models.SEARCH_OP_SERVERS[0], None)` (resolving to `ydc-index.io`) instead of a hardcoded server constant, so custom `server_url` passed to `You()` is respected (previously ignored for search only). +- **`answer()` type annotations**: `country` and `language` parameters changed from `Optional[str]` to `Optional[Union[str, models.Country]]` / `Optional[Union[str, models.Language]]` to match `search()` and reflect the actual enum constraint. String values are still accepted and normalized via `.upper()`. +- **Dev dependencies updated**: mypy `1.15.0` → `>=2.3.0`, pylint `3.2.3` → `>=4.0.0`, pytest floor `>=8.0.0` → `>=9.0.0`, pytest-asyncio floor `>=0.24.0` → `>=1.0.0`. Runtime dependencies (httpx, httpcore, pydantic) unchanged — already at latest stable. + ## [2.5.0] - 2026-07-20 ### Added @@ -26,8 +60,6 @@ detail = research_and_wait( print(detail.result.model_dump()["output"]["content"]) ``` -- **`lite` finance research effort tier**: New `FinanceResearchEffort.LITE` enum value for the Finance Research API. Returns answers quickly for straightforward financial questions. The default remains `deep`. No migration required; existing `DEEP` and `EXHAUSTIVE` values are unchanged. - - **Research Background Mode**: The `you.research()` method now accepts an optional `background=True` parameter. When enabled, the API queues the research task and returns a `TaskResponse` (with `task_id`, `type`, `status`, `stream_url`, `created_at`) immediately instead of waiting for the inline `ResearchResponse`. Use the new methods to poll or stream the task to completion. - **`you.get_research_task(task_id=...)`**: Poll the status of a background research task via `GET /v1/research/{task_id}`. Returns a `TaskDetail` with `status`, `result` (populated when completed), `error` (populated when failed), and timing fields. @@ -161,7 +193,6 @@ for source in res.output.sources: ### Changed -- **Default server URL**: Changed from `https://ydc-index.io` to `https://api.you.com`. If you were relying on the default, no action needed as both resolve to the same API. - **Python version requirement**: Now requires Python >=3.10 (previously >=3.9.2) - **Search API `count` parameter**: Now defaults to `10` instead of `None` - **Contents API `crawl_timeout`**: Type changed from `float` to `int`, default is now `10` seconds diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d585717..b376021 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to This Repository -Thank you for your interest in contributing to this repository. Please note that this repository contains generated code. As such, we do not accept direct changes or pull requests. Instead, we encourage you to follow the guidelines below to report issues and suggest improvements. +Thank you for your interest in contributing to the You.com Python SDK! This SDK is hand-maintained (not generated) and we welcome pull requests. ## How to Report Issues @@ -13,9 +13,29 @@ If you encounter any bugs or have suggestions for improvements, please open an i - Information about your environment (e.g., operating system, software versions) - For example can be collected using the `npx envinfo` command from your terminal if you have Node.js installed -## Issue Triage and Upstream Fixes +## Pull Requests -We will review and triage issues as quickly as possible. Our goal is to address bugs and incorporate improvements in the upstream source code. Fixes will be included in the next generation of the generated code. +1. Fork the repository and create a branch from `main`. +2. Make your changes. Follow existing code style and patterns. +3. Add or update tests as needed. Run `pytest tests/ --ignore=tests/test_live.py` for unit tests (live tests require `YDC_API_KEY`). +4. Run `mypy src/youdotcom/` to ensure type safety. +5. Update documentation (README, CHANGELOG, `docs/` directory) if your change adds or modifies public API surface. +6. Open a pull request with a clear description of the change. + +## Development Setup + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -e . +pip install mypy pylint pyright pytest pytest-asyncio +``` + +Or with [uv](https://docs.astral.sh/uv/): + +```bash +uv sync --dev +``` ## Contact diff --git a/MIGRATION.md b/MIGRATION.md index 1a52ebd..2d599fd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,98 @@ # Migration Guide -## 2.4.0 → 2.5.0 (Latest) +## 2.5.0 → 2.6.0 + +> **This release adds new direct methods, removes the Agents API, and reverts search to keyed `POST /v1/search`.** The old sub-SDK patterns still work but emit `DeprecationWarning`. Migrate at your convenience. + +### Answer API + +New direct method `you.answer()` for `POST /v1/answer`: + +```python +import os +from youdotcom import You + +with You(api_key_auth=os.getenv("YDC_API_KEY")) as you: + res = you.answer(query="What causes the 2008 financial crisis?") + print(res.answer) # markdown with [[1, 2]] citations + if res.citations: + print(res.citations[0].source) # source URL + if res.results and res.results.web: + print(res.results.web[0].title) # web result title +``` + +Requires an API key. `country` and `language` accept plain strings (e.g. `"us"`, `"en"`) and are normalized to uppercase. + +### Search Reverted to Keyed POST /v1/search + +`you.search()` / `you.search_async()` target `POST /v1/search` on `ydc-index.io`. Search requires an API key. + +The standalone `search_helpers` module has been removed. Its `search()` / `search_async()` functions are now direct methods on `You`: + +```python +# Before (2.5.x): from youdotcom.search_helpers import search +# search(you, query="...") + +# After (2.6.0): +you.search(query="...") +``` + +### Server URLs + +`SEARCH_OP_SERVERS` and `CONTENTS_OP_SERVERS` point to `https://ydc-index.io` (used by `you.search()` and `you.contents()`). The default server URL (used by `you.answer()`, `you.research()`, `you.finance_research()`, etc.) is `https://api.you.com`. No code changes required — the SDK resolves the correct server per endpoint automatically. + +### Agents API Removed + +The `you.agents()` / `you.agents_async()` direct methods and the `you.agents.runs` sub-SDK shim have been removed. The Agents API model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, etc.) have also been removed from `youdotcom.models`. If you need the Agents API, use the REST endpoint directly or a previous SDK version. + +### PaymentRequiredResponseError + +The `PaymentRequiredResponseError` exception (extends `YouError`) is raised by the answer API on HTTP 402. It provides structured data: + +```python +import os +from youdotcom import You +from youdotcom.errors import PaymentRequiredResponseError + +with You(api_key_auth=os.getenv("YDC_API_KEY")) as you: + try: + res = you.answer(query="test") # may raise 402 if out of credits + except PaymentRequiredResponseError as e: + print(e.data.message) # "Insufficient credits" + print(e.data.upgrade_url) # "https://you.com/platform" + print(e.data.limit) # 100 + print(e.data.reset_at) # "2026-08-05T00:00:00Z" +``` + +### 422/500 Error Models Expanded + +`UnprocessableEntityResponseErrorData` now includes optional `detail` (FastAPI validation array) and `errors` (JSON:API array) fields in addition to the existing `error` field. `InternalServerErrorResponseData` now includes an optional `errors` field. These are additive — existing code accessing `.error` or `.detail` still works. + +### No Longer Generated by Speakeasy + +The SDK is now hand-maintained. All "Code generated by Speakeasy — DO NOT EDIT" disclaimers have been removed. The `__gen_version__` / `SPEAKEASY_GENERATOR_VERSION` exports have been removed. + +### Sub-SDK Deprecation + +The sub-SDK layer was an abstraction that added unnecessary indirection — `you.search.unified()` routed through extra layers before reaching the HTTP client. In 2.6.0 these chains are collapsed into direct methods on `You`. The new methods also have simpler signatures: `country`, `language`, `safesearch`, `livecrawl`, and `freshness` accept plain strings (e.g. `"us"`, `"en"`, `"strict"`) and are normalized automatically, so callers no longer need to import enum classes. + +The old patterns still work but emit `DeprecationWarning` and delegate to the new methods. Migrate at your convenience: + +| Old (deprecated) | New | +|---------------|-----| +| `you.search.unified(query=...)` | `you.search(query=...)` | +| `you.search.unified_async(query=...)` | `you.search_async(query=...)` | +| `you.contents.generate(urls=...)` | `you.contents(urls=...)` | +| `you.contents.generate_async(urls=...)` | `you.contents_async(urls=...)` | + +The `search_helpers` module has been removed — use `you.search(query=...)` directly. The `you.search_post()` alias has been removed — use `you.search()`. + +To see deprecation warnings in your code: +```bash +python -W default::DeprecationWarning your_script.py +``` + +## 2.4.0 → 2.5.0 ### New `frontier` Research Effort Tier @@ -44,23 +136,6 @@ detail = poll_research_task(you, task_id=task.task_id, timeout_s=14400) No migration is required for existing code. The `frontier` tier is purely additive; existing `LITE`, `STANDARD`, `DEEP`, and `EXHAUSTIVE` values are unchanged. -### New `lite` Finance Research Effort Tier - -A new `FinanceResearchEffort.LITE` enum value has been added for quick, straightforward financial questions. The default remains `deep`. - -```python -from youdotcom import You -from youdotcom.models import FinanceResearchEffort - -you = You() -res = you.finance_research( - input="What was Apple's revenue in FY2024?", - research_effort=FinanceResearchEffort.LITE, -) -``` - -No migration is required. Existing `DEEP` and `EXHAUSTIVE` values are unchanged. - ### New Background Mode for Research The `you.research()` method now accepts `background=True` to queue long-running research tasks asynchronously. The return type changes from `ResearchResponse` to `Union[ResearchResponse, TaskResponse]` (exposed as the `ResearchResult` alias). @@ -314,7 +389,7 @@ The bare `UnprocessableEntityError` / `SearchUnauthorizedError` / `SearchForbidd - `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.search(..., boost_domains=[...])` (POST takes a list) or `you.search.unified(..., boost_domains="nytimes.com,wired.com")` (deprecated shim, 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. --- @@ -362,6 +437,8 @@ res = you.contents.generate(urls=["https://example.com"], crawl_timeout=5) ## 1.x to 2.0 +> **Note:** If you are upgrading to 2.6.0+, the Agents API (`you.agents.runs.create()`) and all agent model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, `ResponseCreated`, `ResponseStarting`, `ResponseOutputTextDelta`, `ResponseOutputContentFull`, `ResponseDone`, `ReportVerbosity`, `SearchEffort`, `AgentRuns401ResponseError`, etc.) have been removed entirely. The migration steps below that reference agent imports and calls are no longer valid — see the [2.5.0 to 2.6.0](#250-to-260) section above. The Search and Contents API changes below still apply. + This guide helps you upgrade your code from You.com Python SDK 1.x to 2.0. ## Quick Reference diff --git a/README.md b/README.md index d9d0461..71f814a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ The official developer-friendly & type-safe Python SDK specifically designed to
- @@ -15,13 +14,9 @@ The official developer-friendly & type-safe Python SDK specifically designed to ## Summary -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 -Finance-focused multi-step research with competitive accuracy at same price points and latencies as the Research API Comprehensive API for You.com services: -- **Agents API**: Execute queries using Express, Advanced, and Custom AI agents + +- **Answer API**: Get synthesized, citation-backed answers grounded in real-time web results - **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 @@ -141,7 +136,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -169,7 +164,7 @@ async def main(): api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = await you.search_post_async(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + res = await you.search_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=[ @@ -206,7 +201,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -228,24 +223,14 @@ 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 +* [answer](docs/sdks/answer/README.md#answer) - Returns a synthesized answer with citations from web search results +* [search](docs/sdks/you/README.md#search) - Returns a list of unified search results from web and news sources +* [contents](docs/sdks/you/README.md#contents) - Returns the content of the web pages * [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) - -* [create](docs/sdks/runs/README.md#create) - Run an Agent - -### [Contents](docs/sdks/contentssdk/README.md) - -* [generate](docs/sdks/contentssdk/README.md#generate) - Returns the content of the web pages - -### [Search](docs/sdks/search/README.md) - -* [unified](docs/sdks/search/README.md#unified) - Returns a list of unified search results from web and news sources - @@ -264,17 +249,6 @@ underlying connection when the context is exited. ```python import os from youdotcom import You -from youdotcom.models import ( - ExpressAgentRunsRequest, - WebSearchTool, - ResponseCreated, - ResponseStarting, - ResponseOutputItemAdded, - ResponseOutputContentFull, - ResponseOutputTextDelta, - ResponseOutputItemDone, - ResponseDone, -) from youdotcom.utils import eventstreaming @@ -282,59 +256,22 @@ with You( api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - response = you.agents.runs.create(request=ExpressAgentRunsRequest( - input="Restaurants in San Francisco", - stream=True, - tools=[ - WebSearchTool() - ] - )) + # Stream updates for a background research task via SSE + response = you.stream_research_task(task_id="your-task-id") # Type narrow to ensure we have a streaming response assert isinstance(response, eventstreaming.EventStream), "Expected streaming response" with response as stream: - # Iterate through the stream and handle each event type - # Each chunk is an AgentRunsStreamingResponse with a 'data' field + # Iterate through the stream and handle each event for chunk in stream: - # The data field contains the actual event (discriminated by TYPE) event_data = chunk.data - - # Use isinstance() to narrow the type and handle each event - if isinstance(event_data, ResponseCreated): - print(f"✨ Response created (seq: {event_data.seq_id})") - - elif isinstance(event_data, ResponseStarting): - print(f"🚀 Response starting (seq: {event_data.seq_id})") - - elif isinstance(event_data, ResponseOutputItemAdded): - print(f"➕ Output item added: {event_data.seq_id}") - - elif isinstance(event_data, ResponseOutputContentFull): - print("\n🔍 Web Search Results:") - if event_data.response.full: - for idx, result in enumerate(event_data.response.full, 1): - print(f" {idx}. {result.title} - {result.url}") - - elif isinstance(event_data, ResponseOutputTextDelta): - # Print the delta text as it streams in (without newline) - print(event_data.response.delta, end='', flush=True) - - elif isinstance(event_data, ResponseOutputItemDone): - print(f"\n✅ Output item done (index: {event_data.response.output_index})") - - elif isinstance(event_data, ResponseDone): - print("\n🎉 Response completed!") - print(f" Runtime: {event_data.response.run_time_ms} ms") - print(f" Finished: {event_data.response.finished}") - - else: - print(f"⚠️ Unknown event type: {type(event_data).__name__}") + print(event_data) ``` [mdn-sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events [generator]: https://book.pythontips.com/en/latest/generators.html [context-manager]: https://book.pythontips.com/en/latest/context_managers.html - + ## Retries @@ -352,7 +289,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -378,7 +315,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -418,7 +355,7 @@ with You( res = None try: - res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + res = you.search(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=[ @@ -447,7 +384,7 @@ with You( **Primary error:** * [`YouError`](./src/youdotcom/errors/youerror.py): The base class for HTTP error responses. -
Less common errors (31) +
Less common errors (27)
@@ -458,32 +395,33 @@ with You( **Inherit from [`YouError`](./src/youdotcom/errors/youerror.py)**: -* [`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.* -* [`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.* -* [`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.* -* [`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.* -* [`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.* -* [`GetResearchTaskUnauthorizedError`](./src/youdotcom/errors/getresearchtaskop.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 8 methods.* -* [`GetResearchTaskForbiddenError`](./src/youdotcom/errors/getresearchtaskop.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 8 methods.* -* [`GetResearchTaskNotFoundError`](./src/youdotcom/errors/getresearchtaskop.py): Not Found. Task does not exist or does not belong to the caller. Status code `404`. Applicable to 1 of 8 methods.* -* [`GetResearchTaskInternalServerError`](./src/youdotcom/errors/getresearchtaskop.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 8 methods.* -* [`StreamResearchTaskUnauthorizedError`](./src/youdotcom/errors/streamresearchtaskop.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 8 methods.* -* [`StreamResearchTaskForbiddenError`](./src/youdotcom/errors/streamresearchtaskop.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 8 methods.* -* [`StreamResearchTaskNotFoundError`](./src/youdotcom/errors/streamresearchtaskop.py): Not Found. Task does not exist or does not belong to the caller. Status code `404`. Applicable to 1 of 8 methods.* -* [`StreamResearchTaskInternalServerError`](./src/youdotcom/errors/streamresearchtaskop.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 8 methods.* +* [`UnauthorizedResponseError`](./src/youdotcom/errors/unauthorized_response_error.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 2 of 7 methods.* +* [`ForbiddenResponseError`](./src/youdotcom/errors/forbidden_response_error.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 2 of 7 methods.* +* [`UnprocessableEntityResponseError`](./src/youdotcom/errors/unprocessableentity_response_error.py): Unprocessable Entity. Invalid request parameter combination. Status code `422`. Applicable to 2 of 7 methods.* +* [`PaymentRequiredResponseError`](./src/youdotcom/errors/paymentrequired_response_error.py): Payment Required. Out of credits or quota exceeded. Status code `402`. Applicable to 1 of 7 methods.* +* [`InternalServerErrorResponse`](./src/youdotcom/errors/internalservererror_response.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 2 of 7 methods.* + +* [`ResearchUnauthorizedError`](./src/youdotcom/errors/researchop.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 7 methods.* +* [`FinanceResearchUnauthorizedError`](./src/youdotcom/errors/finance_researchop.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 7 methods.* +* [`ContentsUnauthorizedError`](./src/youdotcom/errors/contentsop.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 7 methods.* + +* [`ResearchForbiddenError`](./src/youdotcom/errors/researchop.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 7 methods.* +* [`FinanceResearchForbiddenError`](./src/youdotcom/errors/finance_researchop.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 7 methods.* +* [`ContentsForbiddenError`](./src/youdotcom/errors/contentsop.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 7 methods.* +* [`ResearchUnprocessableEntityError`](./src/youdotcom/errors/researchop.py): Unprocessable Entity. Request validation failed. Status code `422`. Applicable to 1 of 7 methods.* +* [`FinanceResearchUnprocessableEntityError`](./src/youdotcom/errors/finance_researchop.py): Unprocessable Entity. Request validation failed. Status code `422`. Applicable to 1 of 7 methods.* + +* [`ResearchInternalServerError`](./src/youdotcom/errors/researchop.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 7 methods.* +* [`FinanceResearchInternalServerError`](./src/youdotcom/errors/finance_researchop.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 7 methods.* +* [`ContentsInternalServerError`](./src/youdotcom/errors/contentsop.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 7 methods.* +* [`GetResearchTaskUnauthorizedError`](./src/youdotcom/errors/getresearchtaskop.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 7 methods.* +* [`GetResearchTaskForbiddenError`](./src/youdotcom/errors/getresearchtaskop.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 7 methods.* +* [`GetResearchTaskNotFoundError`](./src/youdotcom/errors/getresearchtaskop.py): Not Found. Task does not exist or does not belong to the caller. Status code `404`. Applicable to 1 of 7 methods.* +* [`GetResearchTaskInternalServerError`](./src/youdotcom/errors/getresearchtaskop.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 7 methods.* +* [`StreamResearchTaskUnauthorizedError`](./src/youdotcom/errors/streamresearchtaskop.py): Unauthorized. Problems with API key. Status code `401`. Applicable to 1 of 7 methods.* +* [`StreamResearchTaskForbiddenError`](./src/youdotcom/errors/streamresearchtaskop.py): Forbidden. API key lacks scope for this path. Status code `403`. Applicable to 1 of 7 methods.* +* [`StreamResearchTaskNotFoundError`](./src/youdotcom/errors/streamresearchtaskop.py): Not Found. Task does not exist or does not belong to the caller. Status code `404`. Applicable to 1 of 7 methods.* +* [`StreamResearchTaskInternalServerError`](./src/youdotcom/errors/streamresearchtaskop.py): Internal Server Error during authentication/authorization middleware. Status code `500`. Applicable to 1 of 7 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.
@@ -526,7 +464,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -558,6 +496,7 @@ s = You(client=http_client) or you could wrap the client with your own custom logic: ```python +from typing import Any, Optional, Union from youdotcom import You from youdotcom.httpclient import AsyncHttpClient import httpx @@ -664,9 +603,9 @@ s = You(debug_logger=logging.getLogger("youdotcom")) ``` You can also enable a default debug logger by setting an environment variable `YOU_DEBUG` to true. - - +**Warning:** Debug logs include full request headers and bodies, which may contain API keys and sensitive data. Do not enable debug logging in production or commit debug logs to version control. + # Development @@ -705,7 +644,5 @@ For more details on testing, see the [tests README](tests/README.md). ## Contributions -While we value open-source contributions to this SDK, this library is generated programmatically. Any manual changes added to internal files will be overwritten on the next generation. +While we value open-source contributions to this SDK, this library is hand-maintained. We welcome pull requests — see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines. We look forward to hearing your feedback. Feel free to open a PR or an issue with a proof of concept and we'll do our best to include it in a future release. - -### SDK Created by [Speakeasy](https://www.speakeasy.com/?utm_source=youdotcom&utm_campaign=python) diff --git a/USAGE.md b/USAGE.md index 37e0e9d..17f7411 100644 --- a/USAGE.md +++ b/USAGE.md @@ -9,7 +9,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -37,7 +37,7 @@ async def main(): api_key_auth=os.getenv("YDC_API_KEY", ""), ) as you: - res = await you.search_post_async(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ + res = await you.search_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=[ diff --git a/docs/errors/agentruns400responseerror.md b/docs/errors/agentruns400responseerror.md deleted file mode 100644 index e4e48bf..0000000 --- a/docs/errors/agentruns400responseerror.md +++ /dev/null @@ -1,10 +0,0 @@ -# AgentRuns400ResponseError - -The message returned by the error - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | Invalid or expired API key | \ No newline at end of file diff --git a/docs/errors/agentruns401responseerror.md b/docs/errors/agentruns401responseerror.md deleted file mode 100644 index 7f02021..0000000 --- a/docs/errors/agentruns401responseerror.md +++ /dev/null @@ -1,10 +0,0 @@ -# AgentRuns401ResponseError - -The message returned by the error - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | Invalid or expired API key | \ No newline at end of file diff --git a/docs/errors/agentruns422responseerror.md b/docs/errors/agentruns422responseerror.md deleted file mode 100644 index de23c31..0000000 --- a/docs/errors/agentruns422responseerror.md +++ /dev/null @@ -1,8 +0,0 @@ -# AgentRuns422ResponseError - - -## Fields - -| Field | Type | Required | Description | -| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | -| `detail` | List[[models.Detail](../models/detail.md)] | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/docs/errors/internalservererrorresponse.md b/docs/errors/internalservererrorresponse.md index b99d7b0..25fa818 100644 --- a/docs/errors/internalservererrorresponse.md +++ b/docs/errors/internalservererrorresponse.md @@ -2,9 +2,14 @@ Internal Server Error during authentication/authorization middleware. +Handles two possible 500 body shapes: +- `{"detail": "..."}` — plain detail string +- `{"errors": [{"status": "500", "code": "...", "title": "...", ...}]}` — JSON:API format + ## Fields -| Field | Type | Required | Description | -| ------------------ | ------------------ | ------------------ | ------------------ | -| `detail` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `detail` | *Optional[str]* | :heavy_minus_sign: | A description of the error. | +| `errors` | *Optional[List[dict]]* | :heavy_minus_sign: | JSON:API error array from controller-level error handlers. | \ No newline at end of file diff --git a/docs/errors/paymentrequiredresponseerror.md b/docs/errors/paymentrequiredresponseerror.md new file mode 100644 index 0000000..4b4872e --- /dev/null +++ b/docs/errors/paymentrequiredresponseerror.md @@ -0,0 +1,16 @@ +# PaymentRequiredResponseError + +Payment Required (402). The account cannot make paid API requests. + + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `error` | *Optional[str]* | :heavy_minus_sign: | The error code (e.g. `"payment_required"`). | +| `message` | *Optional[str]* | :heavy_minus_sign: | A human-readable description of the error. | +| `upgrade_url` | *Optional[str]* | :heavy_minus_sign: | URL for adding credits or upgrading the account. | +| `limit` | *Optional[int]* | :heavy_minus_sign: | The usage limit, when available. | +| `used` | *Optional[int]* | :heavy_minus_sign: | The usage consumed, when available. | +| `period` | *Optional[str]* | :heavy_minus_sign: | The usage period, when available. | +| `reset_at` | *Optional[str]* | :heavy_minus_sign: | The reset timestamp, when available. | diff --git a/docs/errors/unprocessableentityresponseerror.md b/docs/errors/unprocessableentityresponseerror.md index 5d44499..214b6b0 100644 --- a/docs/errors/unprocessableentityresponseerror.md +++ b/docs/errors/unprocessableentityresponseerror.md @@ -2,9 +2,16 @@ Unprocessable Entity. Invalid request parameter combination. +Handles three possible 422 body shapes: +- `{"error": "..."}` — search spec format +- `{"detail": [{"type": "...", "loc": [...], "msg": "...", ...}]}` — FastAPI validation errors +- `{"errors": [{"status": "422", "code": "...", "title": "...", ...}]}` — JSON:API format + ## Fields -| Field | Type | Required | Description | -| ------------------ | ------------------ | ------------------ | ------------------ | -| `error` | *Optional[str]* | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `error` | *Optional[str]* | :heavy_minus_sign: | Error code from the search spec 422 format. | +| `detail` | *Optional[List[dict]]* | :heavy_minus_sign: | Validation error array from FastAPI's RequestValidationError. | +| `errors` | *Optional[List[dict]]* | :heavy_minus_sign: | JSON:API error array from controller-level error handlers. | \ No newline at end of file diff --git a/docs/models/advancedagentrunsrequest.md b/docs/models/advancedagentrunsrequest.md deleted file mode 100644 index 596bc50..0000000 --- a/docs/models/advancedagentrunsrequest.md +++ /dev/null @@ -1,13 +0,0 @@ -# AdvancedAgentRunsRequest - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `agent` | *Literal["advanced"]* | :heavy_check_mark: | Setting this value to "advanced" is mandatory to use the advanced agent. | advanced | -| `input` | *str* | :heavy_check_mark: | The question you'd like to ask the agent | Analyze the economic impact of renewable energy adoption | -| `stream` | *Optional[bool]* | :heavy_minus_sign: | Must be set to `true` when you want to stream the agent response as it's being generated, and `false` when you want the response to return after the agent has finished. | | -| `tools` | List[[models.Tool](../models/tool.md)] | :heavy_minus_sign: | The advanced agent accepts either `compute` or `research` tools Compute allows your agent to use a Python code interpreter for tasks such as data analysis, mathematical calculations, and plot generation.

Research iteratively searches the web, analyzes the results, and stops when finished. It then provides a comprehensive report to your agent with current, cited information.
| | -| `verbosity` | [Optional[models.Verbosity]](../models/verbosity.md) | :heavy_minus_sign: | 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. | medium | -| `workflow_config` | [Optional[models.WorkflowConfig]](../models/workflowconfig.md) | :heavy_minus_sign: | Defines the maximum number of steps the agent uses in its workflow plan to answer your query. Higher values allow for more tool calls, but it takes longer for the agent to provide the response. For instance, setting max_workflow_steps=5 could allow the agent to call the research tool 3 times and the compute tool 2 times. | | \ No newline at end of file diff --git a/docs/models/agentrunsbatchresponse.md b/docs/models/agentrunsbatchresponse.md deleted file mode 100644 index 3fe03a1..0000000 --- a/docs/models/agentrunsbatchresponse.md +++ /dev/null @@ -1,11 +0,0 @@ -# AgentRunsBatchResponse - - -## 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.Input](../models/input.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/agentrunsresponseoutput.md b/docs/models/agentrunsresponseoutput.md deleted file mode 100644 index b7f9225..0000000 --- a/docs/models/agentrunsresponseoutput.md +++ /dev/null @@ -1,12 +0,0 @@ -# AgentRunsResponseOutput - -The response populated by the agent. - - -## Fields - -| Field | Type | Required | Description | Example | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `text` | *Optional[str]* | :heavy_minus_sign: | The text response of the agent. This field returns when `type == message.answer`. The response returns as markdown formatted text.

For an overview of Markdown syntax, see the [Basic Syntax Markdown Guide](https://www.markdownguide.org/basic-syntax/) | #### Capital of France

The capital of France is **Paris**. It is not only the capital but also the most populous city in the country. Paris is situated on the Seine River in the northern part of France, within the Île-de-France region. It serves as the main cultural, economic, and political center of France [[1]](https://www.coe.int/en/web/interculturalcities/paris)[[2]](https://en.wikipedia.org/wiki/Paris). | -| `type` | [models.Type](../models/type.md) | :heavy_check_mark: | 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 | web_search.results | -| `content` | List[[models.AgentRunsResponseWebSearchResult](../models/agentrunsresponsewebsearchresult.md)] | :heavy_minus_sign: | The text response of the agent.
This field returns when `type == web_search.results` | | \ No newline at end of file diff --git a/docs/models/agentrunsresponsewebsearchresult.md b/docs/models/agentrunsresponsewebsearchresult.md deleted file mode 100644 index 95bff3e..0000000 --- a/docs/models/agentrunsresponsewebsearchresult.md +++ /dev/null @@ -1,16 +0,0 @@ -# AgentRunsResponseWebSearchResult - -The text response of the agent. This field only returns when the type is `web_search.results` - - -## Fields - -| Field | Type | Required | Description | Example | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `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 | 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 | -| `url` | *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 | \ No newline at end of file diff --git a/docs/models/agentrunsstreamingresponse.md b/docs/models/agentrunsstreamingresponse.md deleted file mode 100644 index 897ae07..0000000 --- a/docs/models/agentrunsstreamingresponse.md +++ /dev/null @@ -1,12 +0,0 @@ -# AgentRunsStreamingResponse - -A server-sent event containing stock market update content - - -## Fields - -| Field | Type | Required | Description | -| ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | -| `id` | *str* | :heavy_check_mark: | Sequence number of the SSE event, starts from 0 | -| `event` | *str* | :heavy_check_mark: | The type of the SSE event | -| `data` | [models.Data](../models/data.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/docs/models/agentsrunsrequest.md b/docs/models/agentsrunsrequest.md deleted file mode 100644 index 9140c34..0000000 --- a/docs/models/agentsrunsrequest.md +++ /dev/null @@ -1,25 +0,0 @@ -# AgentsRunsRequest - -The parameters to ask the agent a question - - -## Supported Types - -### `models.ExpressAgentRunsRequest` - -```python -value: models.ExpressAgentRunsRequest = /* values here */ -``` - -### `models.AdvancedAgentRunsRequest` - -```python -value: models.AdvancedAgentRunsRequest = /* values here */ -``` - -### `models.CustomAgentRunsRequest` - -```python -value: models.CustomAgentRunsRequest = /* values here */ -``` - diff --git a/docs/models/agentsrunsresponse.md b/docs/models/agentsrunsresponse.md deleted file mode 100644 index 22dfbc9..0000000 --- a/docs/models/agentsrunsresponse.md +++ /dev/null @@ -1,17 +0,0 @@ -# AgentsRunsResponse - - -## Supported Types - -### `models.AgentRunsBatchResponse` - -```python -value: models.AgentRunsBatchResponse = /* values here */ -``` - -### `Union[eventstreaming.EventStream[models.AgentRunsStreamingResponse], eventstreaming.EventStreamAsync[models.AgentRunsStreamingResponse]]` - -```python -value: Union[eventstreaming.EventStream[models.AgentRunsStreamingResponse], eventstreaming.EventStreamAsync[models.AgentRunsStreamingResponse]] = /* values here */ -``` - diff --git a/docs/models/answercitation.md b/docs/models/answercitation.md new file mode 100644 index 0000000..69c0d7d --- /dev/null +++ b/docs/models/answercitation.md @@ -0,0 +1,11 @@ +# AnswerCitation + +A source cited in the answer, with supporting excerpts. + + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `source` | *str* | :heavy_check_mark: | The URL of the cited source. | +| `excerpts` | Optional[List[*str*]] | :heavy_minus_sign: | Verbatim excerpts from the cited source that support the answer. | diff --git a/docs/models/answerrequestbody.md b/docs/models/answerrequestbody.md new file mode 100644 index 0000000..2015fc9 --- /dev/null +++ b/docs/models/answerrequestbody.md @@ -0,0 +1,16 @@ +# AnswerRequestBody + +Request body for `POST /v1/answer`. + + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant web results. Max 400 characters. Search operators (`site:`, `OR`, etc.) are not supported. | +| `freshness` | [Optional[models.FreshnessValue]](../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results. One of `day`, `week`, `month`, `year`, or `YYYY-MM-DDtoYYYY-MM-DD`. | +| `country` | [Optional[models.Country]](../models/country.md) | :heavy_minus_sign: | A supported country code that determines the geographical focus of the web results. | +| `language` | [Optional[models.Language]](../models/language.md) | :heavy_minus_sign: | A supported BCP 47 language tag that determines the language of the web results. | +| `include_domains` | Optional[List[*str*]] | :heavy_minus_sign: | Domains to exclusively include. Cannot combine with `exclude_domains` or `boost_domains`. Max 500. | +| `exclude_domains` | Optional[List[*str*]] | :heavy_minus_sign: | Domains to exclude. Cannot combine with `include_domains`. Can combine with `boost_domains`. Max 500. | +| `boost_domains` | Optional[List[*str*]] | :heavy_minus_sign: | Domains to prefer in ranking. Cannot combine with `include_domains`. Can combine with `exclude_domains`. Max 500. | diff --git a/docs/models/answerresponse.md b/docs/models/answerresponse.md new file mode 100644 index 0000000..0367952 --- /dev/null +++ b/docs/models/answerresponse.md @@ -0,0 +1,19 @@ +# AnswerResponse + +A synthesized answer with citations and supporting search results. + +## AnswerResults + +Search results grouped by result type. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `web` | Optional[List[[models.AnswerSearchResult](../models/answersearchresult.md)]] | :heavy_minus_sign: | All web search results considered during answer synthesis. | + +## AnswerResponse + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `answer` | *str* | :heavy_check_mark: | The synthesized response with numbered inline citations that reference items in the `citations` array. | +| `citations` | Optional[List[[models.AnswerCitation](../models/answercitation.md)]] | :heavy_minus_sign: | The sources cited in the answer, in citation order. | +| `results` | [Optional[models.AnswerResults]](#answerresults) | :heavy_minus_sign: | Search results grouped by result type. | diff --git a/docs/models/answersearchresult.md b/docs/models/answersearchresult.md new file mode 100644 index 0000000..241633e --- /dev/null +++ b/docs/models/answersearchresult.md @@ -0,0 +1,13 @@ +# AnswerSearchResult + +A web search result used during answer synthesis. + + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `url` | *str* | :heavy_check_mark: | The URL of the source webpage. | +| `title` | *str* | :heavy_check_mark: | The title of the source webpage. | +| `snippets` | Optional[List[*str*]] | :heavy_minus_sign: | Text snippets from the search result that preview its content. | +| `page_age` | *Optional[str]* | :heavy_minus_sign: | The publication date or age supplied by the search result. | diff --git a/docs/models/computetool.md b/docs/models/computetool.md deleted file mode 100644 index 5c8b09f..0000000 --- a/docs/models/computetool.md +++ /dev/null @@ -1,8 +0,0 @@ -# ComputeTool - - -## Fields - -| Field | Type | Required | Description | -| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| `type` | *Literal["compute"]* | :heavy_check_mark: | Setting this value to "compute" is mandatory to use the compute agent. | \ No newline at end of file diff --git a/docs/models/contentsmetadata.md b/docs/models/contentsmetadata.md index 6050bc0..cfac725 100644 --- a/docs/models/contentsmetadata.md +++ b/docs/models/contentsmetadata.md @@ -8,4 +8,4 @@ Metadata about the web page. Only returned when 'metadata' is included in the fo | Field | Type | Required | Description | Example | | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | | `site_name` | *OptionalNullable[str]* | :heavy_minus_sign: | The OpenGraph site name of the web page. | You.com | -| `favicon_url` | *Optional[str]* | :heavy_minus_sign: | The URL of the favicon of the web page's domain. | https://api.ydc-index.io/favicon?domain=you.com&size=128 | \ No newline at end of file +| `favicon_url` | *Optional[str]* | :heavy_minus_sign: | The URL of the favicon of the web page's domain. | https://api.you.com/favicon?domain=you.com&size=128 | \ No newline at end of file diff --git a/docs/models/customagentrunsrequest.md b/docs/models/customagentrunsrequest.md deleted file mode 100644 index d9b6625..0000000 --- a/docs/models/customagentrunsrequest.md +++ /dev/null @@ -1,10 +0,0 @@ -# CustomAgentRunsRequest - - -## Fields - -| Field | Type | Required | Description | Example | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `agent` | *str* | :heavy_check_mark: | Set the value to a Custom Agent's ID. Learn how to obtain an agent ID here [Create Custom Agents](https://docs.you.com/agents/custom/create-agents). | 63773261-b4de-4d8f-9dfd-cff206a5cb51 | -| `input` | *str* | :heavy_check_mark: | The question you'd like to ask the agent | What are some insights about my data? | -| `stream` | *Optional[bool]* | :heavy_minus_sign: | Must be set to `true` when you want to stream the agent response as it's being generated, and `false` when you want the response to return after the agent has finished. | | \ No newline at end of file diff --git a/docs/models/data.md b/docs/models/data.md deleted file mode 100644 index caff37e..0000000 --- a/docs/models/data.md +++ /dev/null @@ -1,47 +0,0 @@ -# Data - - -## Supported Types - -### `models.ResponseCreated` - -```python -value: models.ResponseCreated = /* values here */ -``` - -### `models.ResponseStarting` - -```python -value: models.ResponseStarting = /* values here */ -``` - -### `models.ResponseOutputItemAdded` - -```python -value: models.ResponseOutputItemAdded = /* values here */ -``` - -### `models.ResponseOutputContentFull` - -```python -value: models.ResponseOutputContentFull = /* values here */ -``` - -### `models.ResponseOutputItemDone` - -```python -value: models.ResponseOutputItemDone = /* values here */ -``` - -### `models.ResponseOutputTextDelta` - -```python -value: models.ResponseOutputTextDelta = /* values here */ -``` - -### `models.ResponseDone` - -```python -value: models.ResponseDone = /* values here */ -``` - diff --git a/docs/models/expressagentrunsrequest.md b/docs/models/expressagentrunsrequest.md deleted file mode 100644 index 271ad1a..0000000 --- a/docs/models/expressagentrunsrequest.md +++ /dev/null @@ -1,11 +0,0 @@ -# ExpressAgentRunsRequest - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `agent` | *Literal["express"]* | :heavy_check_mark: | Setting this value to "express" is mandatory to use the express agent. | express | -| `input` | *str* | :heavy_check_mark: | The question you'd like to ask the agent | What are some great recipes I can make in under half an hour | -| `stream` | *Optional[bool]* | :heavy_minus_sign: | Must be set to `true` when you want to stream the express agent response as it's being generated, and `false` when you want the response to return after the agent has finished. | | -| `tools` | List[[models.WebSearchTool](../models/websearchtool.md)] | :heavy_minus_sign: | You can optionally ground the express agent response using results fetched from the web (max 1 web search) | | \ No newline at end of file diff --git a/docs/models/financeresearcheffort.md b/docs/models/financeresearcheffort.md index 6c7ebde..b83a9a9 100644 --- a/docs/models/financeresearcheffort.md +++ b/docs/models/financeresearcheffort.md @@ -3,7 +3,6 @@ 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: -- `lite`: Returns answers quickly. Good for straightforward financial questions that just need a fast, reliable answer. - `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. @@ -20,6 +19,5 @@ value = FinanceResearchEffort.DEEP | Name | Value | | ------------ | ------------ | -| `LITE` | lite | | `DEEP` | deep | -| `EXHAUSTIVE` | exhaustive | \ No newline at end of file +| `EXHAUSTIVE` | exhaustive | diff --git a/docs/models/input.md b/docs/models/input.md deleted file mode 100644 index 54bf616..0000000 --- a/docs/models/input.md +++ /dev/null @@ -1,9 +0,0 @@ -# Input - - -## Fields - -| Field | Type | Required | Description | Example | -| --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | --------------------------------------------- | -| `role` | [models.Role](../models/role.md) | :heavy_check_mark: | The access based role of the user | user | -| `content` | *str* | :heavy_check_mark: | The question populated in the request payload | What is the capital of France? | \ No newline at end of file diff --git a/docs/models/reportverbosity.md b/docs/models/reportverbosity.md deleted file mode 100644 index 7c3d17f..0000000 --- a/docs/models/reportverbosity.md +++ /dev/null @@ -1,19 +0,0 @@ -# ReportVerbosity - -Select whether to receive a medium or high length model response. - -## Example Usage - -```python -from youdotcom.models import ReportVerbosity - -value = ReportVerbosity.MEDIUM -``` - - -## Values - -| Name | Value | -| -------- | -------- | -| `MEDIUM` | medium | -| `HIGH` | high | \ No newline at end of file diff --git a/docs/models/researchtool.md b/docs/models/researchtool.md deleted file mode 100644 index b4ede42..0000000 --- a/docs/models/researchtool.md +++ /dev/null @@ -1,10 +0,0 @@ -# ResearchTool - - -## Fields - -| Field | Type | Required | Description | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | *Literal["research"]* | :heavy_check_mark: | Setting this value to "research" is mandatory to use the research agent. | -| `search_effort` | [models.SearchEffort](../models/searcheffort.md) | :heavy_check_mark: | This parameter maps to different configurations regarding the depth of research the tool can perform. Its values range from `low`, `medium` to `high`.

Alternatively, use `auto` mode for a more dynamic search approach, allowing the tool the freedom to adjust its subparameters. | -| `report_verbosity` | [models.ReportVerbosity](../models/reportverbosity.md) | :heavy_check_mark: | Select whether to receive a medium or high length model response. | \ No newline at end of file diff --git a/docs/models/responsecreated.md b/docs/models/responsecreated.md deleted file mode 100644 index 4e10ecc..0000000 --- a/docs/models/responsecreated.md +++ /dev/null @@ -1,11 +0,0 @@ -# ResponseCreated - -SSE event signifying the response stream has been created - - -## Fields - -| Field | Type | Required | Description | Example | -| ----------------------------- | ----------------------------- | ----------------------------- | ----------------------------- | ----------------------------- | -| `seq_id` | *int* | :heavy_check_mark: | N/A | 0 | -| `type` | *Literal["response.created"]* | :heavy_check_mark: | N/A | response.created | \ No newline at end of file diff --git a/docs/models/responsedone.md b/docs/models/responsedone.md deleted file mode 100644 index 6010f6b..0000000 --- a/docs/models/responsedone.md +++ /dev/null @@ -1,10 +0,0 @@ -# ResponseDone - - -## Fields - -| Field | Type | Required | Description | Example | -| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | -| `seq_id` | *int* | :heavy_check_mark: | N/A | 249 | -| `type` | *Literal["response.done"]* | :heavy_check_mark: | N/A | response.done | -| `response` | [models.ResponseDoneResponse](../models/responsedoneresponse.md) | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/docs/models/responsedoneresponse.md b/docs/models/responsedoneresponse.md deleted file mode 100644 index a702eab..0000000 --- a/docs/models/responsedoneresponse.md +++ /dev/null @@ -1,9 +0,0 @@ -# ResponseDoneResponse - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -| `run_time_ms` | *str* | :heavy_check_mark: | Total runtime in milliseconds | 8.029 | -| `finished` | *bool* | :heavy_check_mark: | Whether the response is complete | true | \ No newline at end of file diff --git a/docs/models/responseoutputcontentfull.md b/docs/models/responseoutputcontentfull.md deleted file mode 100644 index 0351443..0000000 --- a/docs/models/responseoutputcontentfull.md +++ /dev/null @@ -1,10 +0,0 @@ -# ResponseOutputContentFull - - -## Fields - -| Field | Type | Required | Description | Example | -| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | -| `seq_id` | *int* | :heavy_check_mark: | N/A | 3 | -| `type` | *Literal["response.output_content.full"]* | :heavy_check_mark: | N/A | response.output_content.full | -| `response` | [models.ResponseOutputContentFullResponse](../models/responseoutputcontentfullresponse.md) | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/docs/models/responseoutputcontentfullresponse.md b/docs/models/responseoutputcontentfullresponse.md deleted file mode 100644 index 3391d4f..0000000 --- a/docs/models/responseoutputcontentfullresponse.md +++ /dev/null @@ -1,10 +0,0 @@ -# ResponseOutputContentFullResponse - - -## Fields - -| Field | Type | Required | Description | Example | -| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -| `output_index` | *int* | :heavy_check_mark: | N/A | 0 | -| `type` | *Literal["web_search.results"]* | :heavy_check_mark: | N/A | web_search.results | -| `full` | List[[models.AgentRunsResponseWebSearchResult](../models/agentrunsresponsewebsearchresult.md)] | :heavy_check_mark: | Complete web search results | | \ No newline at end of file diff --git a/docs/models/responseoutputitemadded.md b/docs/models/responseoutputitemadded.md deleted file mode 100644 index 7eae624..0000000 --- a/docs/models/responseoutputitemadded.md +++ /dev/null @@ -1,12 +0,0 @@ -# ResponseOutputItemAdded - -SSE event signifying an output item has been added - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| `seq_id` | *int* | :heavy_check_mark: | N/A | 2 | -| `type` | *Literal["response.output_item.added"]* | :heavy_check_mark: | N/A | response.output_item.added | -| `response` | [models.ResponseOutputItemAddedResponse](../models/responseoutputitemaddedresponse.md) | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/docs/models/responseoutputitemaddedresponse.md b/docs/models/responseoutputitemaddedresponse.md deleted file mode 100644 index 4acf214..0000000 --- a/docs/models/responseoutputitemaddedresponse.md +++ /dev/null @@ -1,8 +0,0 @@ -# ResponseOutputItemAddedResponse - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- | -| `output_index` | *int* | :heavy_check_mark: | The index of the output item in the response | 0 | \ No newline at end of file diff --git a/docs/models/responseoutputitemdone.md b/docs/models/responseoutputitemdone.md deleted file mode 100644 index 382af87..0000000 --- a/docs/models/responseoutputitemdone.md +++ /dev/null @@ -1,10 +0,0 @@ -# ResponseOutputItemDone - - -## Fields - -| Field | Type | Required | Description | Example | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | -| `seq_id` | *int* | :heavy_check_mark: | N/A | 4 | -| `type` | *Literal["response.output_item.done"]* | :heavy_check_mark: | N/A | response.output_item.done | -| `response` | [models.ResponseOutputItemDoneResponse](../models/responseoutputitemdoneresponse.md) | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/docs/models/responseoutputitemdoneresponse.md b/docs/models/responseoutputitemdoneresponse.md deleted file mode 100644 index 9d2c570..0000000 --- a/docs/models/responseoutputitemdoneresponse.md +++ /dev/null @@ -1,8 +0,0 @@ -# ResponseOutputItemDoneResponse - - -## Fields - -| Field | Type | Required | Description | Example | -| ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | -| `output_index` | *int* | :heavy_check_mark: | N/A | 0 | \ No newline at end of file diff --git a/docs/models/responseoutputtextdelta.md b/docs/models/responseoutputtextdelta.md deleted file mode 100644 index b584c1a..0000000 --- a/docs/models/responseoutputtextdelta.md +++ /dev/null @@ -1,10 +0,0 @@ -# ResponseOutputTextDelta - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| `seq_id` | *int* | :heavy_check_mark: | N/A | 6 | -| `type` | *Literal["response.output_text.delta"]* | :heavy_check_mark: | N/A | response.output_text.delta | -| `response` | [models.ResponseOutputTextDeltaResponse](../models/responseoutputtextdeltaresponse.md) | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/docs/models/responseoutputtextdeltaresponse.md b/docs/models/responseoutputtextdeltaresponse.md deleted file mode 100644 index 431cd0a..0000000 --- a/docs/models/responseoutputtextdeltaresponse.md +++ /dev/null @@ -1,10 +0,0 @@ -# ResponseOutputTextDeltaResponse - - -## Fields - -| Field | Type | Required | Description | Example | -| --------------------------- | --------------------------- | --------------------------- | --------------------------- | --------------------------- | -| `output_index` | *int* | :heavy_check_mark: | N/A | 1 | -| `type` | *Literal["message.answer"]* | :heavy_check_mark: | N/A | message.answer | -| `delta` | *str* | :heavy_check_mark: | Incremental text content | Test | \ No newline at end of file diff --git a/docs/models/responsestarting.md b/docs/models/responsestarting.md deleted file mode 100644 index 5d468de..0000000 --- a/docs/models/responsestarting.md +++ /dev/null @@ -1,11 +0,0 @@ -# ResponseStarting - -SSE event signifying the response is starting - - -## Fields - -| Field | Type | Required | Description | Example | -| ------------------------------ | ------------------------------ | ------------------------------ | ------------------------------ | ------------------------------ | -| `seq_id` | *int* | :heavy_check_mark: | N/A | 1 | -| `type` | *Literal["response.starting"]* | :heavy_check_mark: | N/A | response.starting | \ No newline at end of file diff --git a/docs/models/role.md b/docs/models/role.md deleted file mode 100644 index 1f861cc..0000000 --- a/docs/models/role.md +++ /dev/null @@ -1,18 +0,0 @@ -# Role - -The access based role of the user - -## Example Usage - -```python -from youdotcom.models import Role - -value = Role.USER -``` - - -## Values - -| Name | Value | -| ------ | ------ | -| `USER` | user | \ No newline at end of file diff --git a/docs/models/searcheffort.md b/docs/models/searcheffort.md deleted file mode 100644 index 6f1c1ed..0000000 --- a/docs/models/searcheffort.md +++ /dev/null @@ -1,23 +0,0 @@ -# SearchEffort - -This parameter maps to different configurations regarding the depth of research the tool can perform. Its values range from `low`, `medium` to `high`. - -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 - -| Name | Value | -| -------- | -------- | -| `AUTO` | auto | -| `LOW` | low | -| `MEDIUM` | medium | -| `HIGH` | high | \ No newline at end of file diff --git a/docs/models/tool.md b/docs/models/tool.md deleted file mode 100644 index bb6d451..0000000 --- a/docs/models/tool.md +++ /dev/null @@ -1,17 +0,0 @@ -# Tool - - -## Supported Types - -### `models.ComputeTool` - -```python -value: models.ComputeTool = /* values here */ -``` - -### `models.ResearchTool` - -```python -value: models.ResearchTool = /* values here */ -``` - diff --git a/docs/models/type.md b/docs/models/type.md deleted file mode 100644 index 31b2152..0000000 --- a/docs/models/type.md +++ /dev/null @@ -1,21 +0,0 @@ -# Type - -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 - -| Name | Value | -| -------------------- | -------------------- | -| `MESSAGE_ANSWER` | message.answer | -| `WEB_SEARCH_RESULTS` | web_search.results | \ No newline at end of file diff --git a/docs/models/verbosity.md b/docs/models/verbosity.md deleted file mode 100644 index 35bfe62..0000000 --- a/docs/models/verbosity.md +++ /dev/null @@ -1,19 +0,0 @@ -# Verbosity - -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 - -| Name | Value | -| -------- | -------- | -| `MEDIUM` | medium | -| `HIGH` | high | \ No newline at end of file diff --git a/docs/models/websearchtool.md b/docs/models/websearchtool.md deleted file mode 100644 index 5cdccb4..0000000 --- a/docs/models/websearchtool.md +++ /dev/null @@ -1,8 +0,0 @@ -# WebSearchTool - - -## Fields - -| Field | Type | Required | Description | -| --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `type` | *Literal["web_search"]* | :heavy_check_mark: | Setting this value to "web_search" is mandatory to use the web_search tool. | \ No newline at end of file diff --git a/docs/models/workflowconfig.md b/docs/models/workflowconfig.md deleted file mode 100644 index b8a67d3..0000000 --- a/docs/models/workflowconfig.md +++ /dev/null @@ -1,10 +0,0 @@ -# WorkflowConfig - -Defines the maximum number of steps the agent uses in its workflow plan to answer your query. Higher values allow for more tool calls, but it takes longer for the agent to provide the response. For instance, setting max_workflow_steps=5 could allow the agent to call the research tool 3 times and the compute tool 2 times. - - -## Fields - -| Field | Type | Required | Description | Example | -| -------------------- | -------------------- | -------------------- | -------------------- | -------------------- | -| `max_workflow_steps` | *Optional[int]* | :heavy_minus_sign: | N/A | 10 | \ No newline at end of file diff --git a/docs/sdks/answer/README.md b/docs/sdks/answer/README.md new file mode 100644 index 0000000..a0edaf1 --- /dev/null +++ b/docs/sdks/answer/README.md @@ -0,0 +1,66 @@ +# Answer + +## Overview + +The Answer API returns a synthesized natural-language answer with citations and the web results used to generate it. Send a `query` with optional freshness, locale, and domain controls. + +Called as a direct method on the `You` client: `you.answer(query=...)`. + +### Available Operations + +* [answer](#answer) - Returns a synthesized answer with citations from web search results + +## answer + +Returns a synthesized natural-language answer with citations and the web results used to generate it. Provide a `query` and optional freshness, locale, and domain controls. + +### Example Usage + +```python +import os +from youdotcom import You + + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + + res = you.answer(query="What are the main causes of the 2008 financial crisis?") + + # Handle response + print(res.answer) + for citation in (res.citations or []): + excerpt = (citation.excerpts or [""])[0] + print(f" [{citation.source}] {excerpt}") +``` + +### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `query` | *str* | :heavy_check_mark: | The search query. Max 400 characters. Search operators (`site:`, `OR`, etc.) are not supported. | +| `freshness` | *Optional[str]* | :heavy_minus_sign: | `day`, `week`, `month`, `year`, or `YYYY-MM-DDtoYYYY-MM-DD` | +| `country` | *Optional[str]* | :heavy_minus_sign: | Country code (e.g. `US`, `GB`, `FR`). Normalized to uppercase. | +| `language` | *Optional[str]* | :heavy_minus_sign: | BCP 47 language tag (e.g. `EN`, `EN-GB`, `FR`). Normalized to uppercase. | +| `include_domains` | *Optional[List[str]]* | :heavy_minus_sign: | Domains to exclusively include. Cannot combine with `exclude_domains` or `boost_domains`. Max 500. | +| `exclude_domains` | *Optional[List[str]]* | :heavy_minus_sign: | Domains to exclude. Cannot combine with `include_domains`. Can combine with `boost_domains`. Max 500. | +| `boost_domains` | *Optional[List[str]]* | :heavy_minus_sign: | Domains to prefer in ranking. Cannot combine with `include_domains`. Can combine with `exclude_domains`. Max 500. | +| `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. | +| `timeout_ms` | *Optional[int]* | :heavy_minus_sign: | Override the default request timeout in milliseconds. | +| `http_headers` | *Optional[Mapping[str, str]]* | :heavy_minus_sign: | Additional headers to set or replace on requests. | + +### Response + +**[models.AnswerResponse](../../models/answerresponse.md)** + +### Errors + +| Error Type | Status Code | Content Type | +|------------|-------------|-------------| +| errors.UnauthorizedResponseError | 401 | application/json | +| errors.PaymentRequiredResponseError | 402 | application/json | +| errors.ForbiddenResponseError | 403 | application/json | +| errors.UnprocessableEntityResponseError | 422 | application/json | +| errors.InternalServerErrorResponse | 500 | application/json | +| errors.YouDefaultError | 4XX, 5XX | \*/\* | diff --git a/docs/sdks/contentssdk/README.md b/docs/sdks/contentssdk/README.md index b1617a1..66f6c28 100644 --- a/docs/sdks/contentssdk/README.md +++ b/docs/sdks/contentssdk/README.md @@ -1,5 +1,12 @@ # Contents +> **DEPRECATED** — The `ContentsSDK` sub-SDK pattern still works but emits `DeprecationWarning`. `you.contents()` is now a direct method on `You` with the same parameters. Use the direct method instead: +> +> - `you.contents(urls=...)` (was `you.contents.generate(urls=...)`) +> - `you.contents_async(urls=...)` (was `you.contents.generate_async(urls=...)`) +> +> See [docs/sdks/you/README.md](../you/README.md#contents) for the current API. The content below is kept for reference only. + ## Overview ### Available Operations @@ -162,7 +169,7 @@ with You( ### Response -**[List[models.ContentsResponse]](../../models/.md)** +**[List[models.ContentsResponse]](../../models/contentsresponse.md)** ### Errors diff --git a/docs/sdks/runs/README.md b/docs/sdks/runs/README.md deleted file mode 100644 index 11795dd..0000000 --- a/docs/sdks/runs/README.md +++ /dev/null @@ -1,201 +0,0 @@ -# Agents.Runs - -## Overview - -### Available Operations - -* [create](#create) - Run an Agent - -## create - -Execute queries using You.com's AI agents. This endpoint supports three agent types: - -- **Express Agent**: Fast responses with optional web search (max 1 search) -- **Advanced Agent**: Complex queries with multi-turn reasoning, planning, and tool usage -- **Custom Agent**: User-configured assistants created in the You.com UI - -The response format depends on the `stream` parameter - either a complete JSON payload or Server-Sent Events (SSE). - - -### Example Usage: advanced_batch - - -```python -import os -from youdotcom import You, models - - -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: - - res = you.agents.runs.create(request={ - "agent": "advanced", - "input": "You are a biologist studying the impacts of microplastics. Explain what microplastics are to a group of engineers, explain the impacts of microplastics on the body, and what the common sources and dosages of microplastics are. Highlight what a safe dosage might be and how to achieve it", - "stream": False, - "tools": [ - { - "type": "research", - "search_effort": models.SearchEffort.AUTO, - "report_verbosity": models.ReportVerbosity.MEDIUM, - }, - ], - }) - - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) - -``` -### Example Usage: advanced_stream - - -```python -import os -from youdotcom import You - - -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: - - res = you.agents.runs.create(request={ - "agent": "express", - "input": "Analyze the economic impact of renewable energy adoption", - "stream": True, - "tools": [ - { - "type": "web_search", - }, - ], - }) - - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) - -``` -### Example Usage: custom_batch - - -```python -import os -from youdotcom import You - - -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: - - res = you.agents.runs.create(request={ - "agent": "63773261-b4de-4d8f-9dfd-cff206a5cb51", - "input": "What is the capital of France?", - "stream": False, - }) - - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) - -``` -### Example Usage: custom_stream - - -```python -import os -from youdotcom import You - - -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: - - res = you.agents.runs.create(request={ - "agent": "63773261-b4de-4d8f-9dfd-cff206a5cb51", - "input": "Tell me about the history of Paris", - "stream": True, - }) - - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) - -``` -### Example Usage: express_batch - - -```python -import os -from youdotcom import You - - -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: - - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What is the capital of France?", - "stream": False, - }) - - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) - -``` -### Example Usage: express_stream - - -```python -import os -from youdotcom import You - - -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: - - res = you.agents.runs.create(request={ - "agent": "express", - "input": "What are some great recipes I can make in under half an hour", - "stream": True, - "tools": [ - { - "type": "web_search", - }, - ], - }) - - with res as event_stream: - for event in event_stream: - # handle event - print(event, flush=True) - -``` - -### Parameters - -| Parameter | Type | Required | Description | -| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `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 - -**[models.AgentsRunsResponse](../../models/agentsrunsresponse.md)** - -### Errors - -| Error Type | Status Code | Content Type | -| -------------------------------- | -------------------------------- | -------------------------------- | -| errors.AgentRuns400ResponseError | 400 | application/json | -| errors.AgentRuns401ResponseError | 401 | application/json | -| errors.AgentRuns422ResponseError | 422 | application/json | -| errors.YouDefaultError | 4XX, 5XX | \*/\* | \ No newline at end of file diff --git a/docs/sdks/search/README.md b/docs/sdks/search/README.md index b02eb4d..6e1548d 100644 --- a/docs/sdks/search/README.md +++ b/docs/sdks/search/README.md @@ -1,5 +1,12 @@ # Search +> **DEPRECATED** — The `Search` sub-SDK pattern still works but emits `DeprecationWarning`. The sub-SDK layer was Speakeasy-generated indirection; `you.search()` is now a direct method on `You` with a simpler signature (accepts plain strings for `country`, `safesearch`, `freshness` — no enum imports needed). Use the direct method instead: +> +> - `you.search(query=...)` (was `you.search.unified(query=...)` and `you.search_post(query=...)`) +> - `you.search_async(query=...)` (was `you.search_post_async(query=...)`) +> +> See [docs/sdks/you/README.md](../you/README.md#search) for the current API. The content below is kept for reference only. + ## Overview ### Available Operations diff --git a/docs/sdks/you/README.md b/docs/sdks/you/README.md index 58c8b5a..690a6e8 100644 --- a/docs/sdks/you/README.md +++ b/docs/sdks/you/README.md @@ -2,13 +2,13 @@ ## Overview -You.com API: Unified API for Express, Advanced, and Custom Agents from You.com +You.com API: Unified API for search, answers, research, and content 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 Finance-focused multi-step research with competitive accuracy at same price points and latencies as the Research API Comprehensive API for You.com services: -- **Agents API**: Execute queries using Express, Advanced, and Custom AI agents +- **Answer API**: Get synthesized, citation-backed answers grounded in real-time web results - **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 @@ -16,21 +16,38 @@ Comprehensive API for You.com services: ### Available Operations -* [search_post](#search_post) - Returns a list of unified search results from web and news sources +* [answer](#answer) - Returns a synthesized answer with citations from web search results +* [search](#search) - Returns a list of unified search results from web and news sources +* [contents](#contents) - Returns the content of the web pages * [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 +## answer -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. +Returns a synthesized answer with citations from web search results. -`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 -### Example Usage: authFailure +```python +import os +from youdotcom import You + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + res = you.answer(query="What is the capital of France?") + print(res) +``` + +## search + +Search via `POST /v1/search`. Returns unified search results from web and news sources. Requires an API key. Country and language accept plain strings and are normalized to uppercase. + +### Example Usage - + ```python import os from youdotcom import You, models @@ -40,7 +57,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -54,7 +71,7 @@ with You( ``` ### Example Usage: authorizationFailure - + ```python import os from youdotcom import You, models @@ -64,7 +81,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -78,7 +95,7 @@ with You( ``` ### Example Usage: invalidOrExpired - + ```python import os from youdotcom import You, models @@ -88,7 +105,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -102,7 +119,7 @@ with You( ``` ### Example Usage: invalidParams - + ```python import os from youdotcom import You, models @@ -112,7 +129,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -126,7 +143,7 @@ with You( ``` ### Example Usage: missingApiKey - + ```python import os from youdotcom import You, models @@ -136,7 +153,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -150,7 +167,7 @@ with You( ``` ### Example Usage: missingScopes - + ```python import os from youdotcom import You, models @@ -160,7 +177,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -174,7 +191,7 @@ with You( ``` ### Example Usage: otherAuthParsing - + ```python import os from youdotcom import You, models @@ -184,7 +201,7 @@ 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, exclude_domains=[ + res = you.search(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=[ @@ -231,6 +248,23 @@ with You( | errors.InternalServerErrorResponse | 500 | application/json | | errors.YouDefaultError | 4XX, 5XX | \*/\* | +## contents + +Returns the HTML or Markdown of target web pages. + +### Example Usage + +```python +import os +from youdotcom import You, models + +with You( + api_key_auth=os.getenv("YDC_API_KEY", ""), +) as you: + res = you.contents(urls=["https://example.com"], formats=[models.ContentsFormats.MARKDOWN]) + print(res) +``` + ## research 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. @@ -722,7 +756,7 @@ with You( | 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:
- `lite`: Returns answers quickly. Good for straightforward financial questions that just need a fast, reliable answer.
- `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 | +| `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 diff --git a/examples/api-example-calls.py b/examples/api-example-calls.py index 9136685..73f2522 100755 --- a/examples/api-example-calls.py +++ b/examples/api-example-calls.py @@ -22,186 +22,19 @@ import time from youdotcom import You from youdotcom.models import ( - ResearchTool, - ExpressAgentRunsRequest, - AdvancedAgentRunsRequest, - SearchEffort, - ReportVerbosity, - CustomAgentRunsRequest, LiveCrawl, LiveCrawlFormats, - ResponseCreated, - ResponseStarting, - ResponseOutputItemAdded, - ResponseOutputContentFull, - ResponseOutputItemDone, - ResponseOutputTextDelta, - ResponseDone, - AgentRunsBatchResponse, - AgentRunsStreamingResponse, ContentsFormats, - WebSearchTool, ResearchEffort, FinanceResearchEffort, FinanceResearchResponse, ResearchResponse, TaskResponse, ) -from youdotcom.utils import eventstreaming # Will be initialized with API key in main() you: Optional[You] = None -def express_batch_request(): - """ - Express agent with batch (non-streaming) response - """ - print("\n🚀 Running Express Batch Request...\n") - - assert you is not None, "SDK client not initialized" - - results = you.agents.runs.create(request=ExpressAgentRunsRequest( - input="What is the capital of France?", - stream=False, - tools=[ - WebSearchTool() - ] - )) - - # Access the results - check if it's a batch response - if isinstance(results, AgentRunsBatchResponse): - if results.output: - for output in results.output: - if output.text: - print(output.text) - break - else: - print("No text response found in agent output") - else: - print("No response from agent") - else: - print("Unexpected response type") - -def express_streaming_request(): - """ - Express agent with streaming response - """ - print("\n🚀 Running Express Streaming Request...\n") - - assert you is not None, "SDK client not initialized" - - response = you.agents.runs.create(request=ExpressAgentRunsRequest( - input="Restaurants in San Francisco", - stream=True, - tools=[ - WebSearchTool() - ] - )) - - # Type narrow to ensure we have a streaming response - assert isinstance(response, eventstreaming.EventStream), "Expected streaming response" - with response as stream: - # Iterate through the stream and handle each event type - # Each chunk is an AgentRunsStreamingResponse with a 'data' field - for chunk in stream: - # The data field contains the actual event (discriminated by TYPE) - event_data = chunk.data - - # Use isinstance() to narrow the type and handle each event - # This is the proper way to do a "switch statement" on Union types in Python - if isinstance(event_data, ResponseCreated): - print(f"✨ Response created (seq: {event_data.seq_id})") - - elif isinstance(event_data, ResponseStarting): - print(f"🚀 Response starting (seq: {event_data.seq_id})") - - elif isinstance(event_data, ResponseOutputItemAdded): - print(f"➕ Output item added: {event_data.seq_id}") - - elif isinstance(event_data, ResponseOutputContentFull): - print("\n🔍 Web Search Results:") - if event_data.response.full: - for idx, result in enumerate(event_data.response.full, 1): - print(f" {idx}. {result.title} - {result.url}") - - elif isinstance(event_data, ResponseOutputTextDelta): - # Print the delta text as it streams in (without newline) - print(event_data.response.delta, end='', flush=True) - - elif isinstance(event_data, ResponseOutputItemDone): - print(f"\n✅ Output item done (index: {event_data.response.output_index})") - - elif isinstance(event_data, ResponseDone): - print("\n🎉 Response completed!") - print(f" Runtime: {event_data.response.run_time_ms} seconds") - print(f" Finished: {event_data.response.finished}") - - else: - print(f"⚠️ Unknown event type: {type(event_data).__name__}") - - -def advanced_batch_request(): - """ - Advanced agent with batch response - """ - print("\n🚀 Running Advanced Batch Request...\n") - - assert you is not None, "SDK client not initialized" - - request = AdvancedAgentRunsRequest( - input="What is the capital of France?", - stream=False, - tools=[ - ResearchTool( - search_effort=SearchEffort.LOW, - report_verbosity=ReportVerbosity.MEDIUM - ) - ] - ) - - results = you.agents.runs.create(request=request) - - # Access the results - check if it's a batch response - if isinstance(results, AgentRunsBatchResponse): - if results.output: - for output in results.output: - if output.text: - print(output.text) - break - else: - print("No text response found in agent output") - else: - print("No response from agent") - else: - print("Unexpected response type") - - -def custom_batch_request(): - """ - Custom agent with batch response - Note: Replace the agent ID with your own custom agent ID - """ - print("\n🚀 Running Custom Batch Request...\n") - - assert you is not None, "SDK client not initialized" - - # Replace this with your actual custom agent ID - custom_agent_id = "63773261-b4de-4d8f-9dfd-cff206a5cb51" - - request = CustomAgentRunsRequest( - agent=custom_agent_id, - input="What is the capital of France?", - stream=False - ) - - try: - results = you.agents.runs.create(request=request) - print(results) - except Exception as e: - print(f"Error: {e}") - print("Note: Make sure to use a valid custom agent ID") - - def search_request(): """ Search API endpoint with livecrawl @@ -210,7 +43,7 @@ def search_request(): assert you is not None, "SDK client not initialized" - results = you.search.unified( + results = you.search( query="artificial intelligence in farming", count=1, livecrawl=LiveCrawl.WEB, @@ -242,7 +75,7 @@ def content_request(): # Example 1: Get markdown content print("Example 1: Fetching markdown content...") - results = you.contents.generate( + results = you.contents( urls=["https://you.com"], formats=[ContentsFormats.MARKDOWN] ) @@ -257,7 +90,7 @@ def content_request(): # Example 2: Get multiple formats including metadata (json+ld, opengraph info) print("Example 2: Fetching HTML + metadata...") - results = you.contents.generate( + results = you.contents( urls=["https://you.com"], formats=[ContentsFormats.HTML, ContentsFormats.METADATA], crawl_timeout=30 # Optional: set custom timeout (1-60 seconds) @@ -487,10 +320,10 @@ def search_request_with_boost(): assert you is not None, "SDK client not initialized" - results = you.search.unified( + results = you.search( query="latest advances in fusion energy research", count=5, - boost_domains="nature.com,science.org,arxiv.org", + boost_domains=["nature.com", "science.org", "arxiv.org"], ) print("Top results:") @@ -509,7 +342,7 @@ def content_request_with_max_age(): assert you is not None, "SDK client not initialized" - results = you.contents.generate( + results = you.contents( urls=["https://example.com/page"], formats=[ContentsFormats.MARKDOWN], crawl_timeout=20, @@ -524,10 +357,6 @@ def content_request_with_max_age(): # Available functions menu FUNCTIONS = [ - {"name": "Express Batch Request", "fn": express_batch_request}, - {"name": "Express Streaming Request", "fn": express_streaming_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}, diff --git a/overlays/python_overlay.yaml b/overlays/python_overlay.yaml deleted file mode 100644 index 905aed5..0000000 --- a/overlays/python_overlay.yaml +++ /dev/null @@ -1,121 +0,0 @@ -overlay: 1.0.0 -x-speakeasy-jsonpath: rfc9535 -info: - title: Python Specific Modifications - 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: - - contents - x-speakeasy-name-override: generate - - target: $["paths"]["/v1/search"]["get"] - update: - tags: - - search - x-speakeasy-name-override: unified - - target: $["paths"]["/v1/agents/runs"]["post"] - update: - 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 the REQUEST side: `output_schema` is an inline `type: object` - # schema (no `$ref`), so Speakeasy generates an empty `OutputSchema` pydantic - # class that drops all `additionalProperties` (i.e., the JSON Schema fields the - # server actually consumes). Without this overlay, the request body arrives - # at the server as `{}` and we get a 422 ("Structured output schema root must - # be an object"). Reviewed as part of 2.4.0 final QA. - - target: $["paths"]["/v1/research"]["post"]["requestBody"]["content"]["application/json"]["schema"]["properties"]["output_schema"] - update: - additionalProperties: true - # Remove the `authors` field from the web search result schema. - # David requested removing it from the v1/v0 web search response spec; - # the overlay makes this regen-durable until the upstream spec drops it. - - target: $["components"]["schemas"]["WebResult"]["properties"]["authors"] - remove: true - # Background-mode research: TaskDetail.result is an inline object schema - # that Speakeasy generates as `class Result(BaseModel): pass`, which - # round-trips as an empty dict. Inject additionalProperties: true so the - # full ResearchResponse payload (output.content, sources, etc.) survives - # deserialization. The Result model in taskdetail.py already has - # extra="allow"; this overlay ensures future regens preserve it. - - target: $["components"]["schemas"]["TaskDetail"]["properties"]["result"] - update: - additionalProperties: true - # Same fix for TaskDetail.input: the server stores the original request - # as a Dict[str, Any] (input, research_effort, source_control, etc.) but - # Speakeasy generates an empty TaskDetailInput BaseModel that drops all - # fields. Inject additionalProperties: true so the input round-trips. - # The TaskDetailInput model in taskdetail.py already has extra="allow". - - target: $["components"]["schemas"]["TaskDetail"]["properties"]["input"] - update: - additionalProperties: true - # Add the `frontier` research_effort tier to the ResearchEffort enum. - # Frontier is a long-running, higher-quality tier that only works with - # the task-based (background=true) API. The upstream spec doesn't yet - # include it, so this overlay injects the enum value and updates the - # description to document the new tier. Regen-durable: future Speakeasy - # regenerations will pick up `frontier` from the overlay-applied spec. - - target: $["components"]["schemas"]["ResearchEffort"]["enum"] - update: - - lite - - standard - - deep - - exhaustive - - frontier - - target: $["components"]["schemas"]["ResearchEffort"]["description"] - update: >- - 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. - - `frontier`: The highest-quality tier. Runs over longer durations - with improved quality and accuracy. Only works with the task-based - API (`background=true`); sending `frontier` without `background=true` - returns a 422. - # Add the `lite` research_effort tier to the FinanceResearchEffort enum. - # The upstream spec doesn't yet include it, so this overlay injects the - # enum value and updates the description. Regen-durable. - - target: $["components"]["schemas"]["FinanceResearchEffort"]["enum"] - update: - - lite - - deep - - exhaustive - - target: $["components"]["schemas"]["FinanceResearchEffort"]["description"] - update: >- - 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: - - `lite`: Returns answers quickly. Good for straightforward financial - questions that just need a fast, reliable answer. - - `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. diff --git a/pyproject.toml b/pyproject.toml index e14db01..d3c2916 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "youdotcom" -version = "2.5.0" +version = "2.6.0" description = "The official You.com Python SDK." authors = [{ name = "You.com" },] readme = "README.md" @@ -14,11 +14,11 @@ license = { text = "Apache-2.0" } [dependency-groups] dev = [ - "mypy ==1.15.0", - "pylint ==3.2.3", - "pyright ==1.1.398", - "pytest >=8.0.0", - "pytest-asyncio >=0.24.0", + "mypy >=2.3.0", + "pylint >=4.0.0", + "pyright >=1.1.398", + "pytest >=9.0.0", + "pytest-asyncio >=1.0.0", ] [tool.setuptools.packages.find] diff --git a/src/youdotcom/__init__.py b/src/youdotcom/__init__.py index 833c68c..4153b35 100644 --- a/src/youdotcom/__init__.py +++ b/src/youdotcom/__init__.py @@ -1,10 +1,9 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from ._version import ( __title__, __version__, __openapi_doc_version__, - __gen_version__, __user_agent__, ) from .sdk import * @@ -13,5 +12,4 @@ VERSION: str = __version__ OPENAPI_DOC_VERSION = __openapi_doc_version__ -SPEAKEASY_GENERATOR_VERSION = __gen_version__ USER_AGENT = __user_agent__ diff --git a/src/youdotcom/_hooks/__init__.py b/src/youdotcom/_hooks/__init__.py index 2ee66cd..2fc0799 100644 --- a/src/youdotcom/_hooks/__init__.py +++ b/src/youdotcom/_hooks/__init__.py @@ -1,5 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from .sdkhooks import * from .types import * -from .registration import * diff --git a/src/youdotcom/_hooks/registration.py b/src/youdotcom/_hooks/registration.py deleted file mode 100644 index fb7f679..0000000 --- a/src/youdotcom/_hooks/registration.py +++ /dev/null @@ -1,45 +0,0 @@ -from .types import Hooks, BeforeRequestHook, BeforeRequestContext -import httpx -from typing import Union - - -# 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 on every request. - - 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}``. - """ - - def before_request(self, hook_ctx: BeforeRequestContext, request: httpx.Request) -> Union[httpx.Request, Exception]: - sdk_version = hook_ctx.config.sdk_version - configured_ua = hook_ctx.config.user_agent - - # `not startswith(_DEFAULT_UA_PREFIX)` already handles the default-UA - # case (the speakeasy default always starts with the prefix), so a - # separate `configured_ua != __user_agent__` check is redundant. - is_custom = bool(configured_ua) and not configured_ua.startswith(_DEFAULT_UA_PREFIX) - - request.headers["User-Agent"] = ( - configured_ua if is_custom else f"youdotcom-python-sdk/{sdk_version}" - ) - - return request - - -def init_hooks(hooks: Hooks): - # pylint: disable=unused-argument - """Add hooks by calling hooks.register{sdk_init/before_request/after_success/after_error}Hook - with an instance of a hook that implements that specific Hook interface - Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance""" - hooks.register_before_request_hook(YDCUserAgentOverrideHook()) diff --git a/src/youdotcom/_hooks/sdkhooks.py b/src/youdotcom/_hooks/sdkhooks.py index 48d0589..d9d8ffb 100644 --- a/src/youdotcom/_hooks/sdkhooks.py +++ b/src/youdotcom/_hooks/sdkhooks.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import httpx from .types import ( @@ -11,7 +11,6 @@ AfterErrorHook, Hooks, ) -from .registration import init_hooks from typing import List, Optional, Tuple from youdotcom.sdkconfiguration import SDKConfiguration @@ -22,7 +21,6 @@ def __init__(self) -> None: self.before_request_hooks: List[BeforeRequestHook] = [] self.after_success_hooks: List[AfterSuccessHook] = [] self.after_error_hooks: List[AfterErrorHook] = [] - init_hooks(self) def register_sdk_init_hook(self, hook: SDKInitHook) -> None: self.sdk_init_hooks.append(hook) diff --git a/src/youdotcom/_hooks/types.py b/src/youdotcom/_hooks/types.py index 2b03ad3..c41ea53 100644 --- a/src/youdotcom/_hooks/types.py +++ b/src/youdotcom/_hooks/types.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from abc import ABC, abstractmethod import httpx diff --git a/src/youdotcom/_shims.py b/src/youdotcom/_shims.py new file mode 100644 index 0000000..ea39b6f --- /dev/null +++ b/src/youdotcom/_shims.py @@ -0,0 +1,255 @@ +"""Deprecated sub-SDK shims that delegate to direct methods on ``You``. + +These exist for backward compatibility. The old access patterns still work +but emit ``DeprecationWarning``: + + you.search.unified(query=...) → you.search(query=...) + you.search.unified_async(query=...) → you.search_async(query=...) + you.contents.generate(urls=...) → you.contents(urls=...) + you.contents.generate_async(urls=...) → you.contents_async(urls=...) + +The new direct-method API (``you.search(query=...)`` etc.) is preferred +and emits no warning. +""" + +from __future__ import annotations + +import warnings +from typing import Any, Iterable, List, Mapping, Optional + +from youdotcom import models, utils +from youdotcom.types import OptionalNullable, UNSET + + +def _warn(old: str, new: str) -> None: + warnings.warn( + f"{old} is deprecated; use {new} instead", + DeprecationWarning, + stacklevel=3, + ) + + +def _split_csv(v: Optional[str]) -> Optional[List[str]]: + if v is None: + return None + return [s.strip() for s in v.split(",") if s.strip()] + + +class SearchShim: + """Callable shim for ``you.search``. + + ``you.search(query=...)`` → delegates to ``you._search_impl()`` (no warning). + ``you.search.unified(query=...)`` → delegates with DeprecationWarning. + """ + + def __init__(self, you: Any) -> None: + self._you = you + + def __call__( + self, + *, + query: str, + count: Optional[int] = 10, + freshness: Optional[str] = None, + offset: Optional[int] = None, + country: Optional[str] = None, + language: Optional[str] = None, + safesearch: Optional[str] = None, + livecrawl: Optional[str] = None, + livecrawl_formats: Optional[Iterable[str]] = 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: + return self._you._search_impl( + query=query, + count=count, + freshness=freshness, + offset=offset, + country=country, + language=language, + safesearch=safesearch, + livecrawl=livecrawl, + livecrawl_formats=livecrawl_formats, + include_domains=include_domains, + exclude_domains=exclude_domains, + boost_domains=boost_domains, + crawl_timeout=crawl_timeout, + retries=retries, + server_url=server_url, + timeout_ms=timeout_ms, + http_headers=http_headers, + ) + + def unified( + self, + *, + query: str, + count: Optional[int] = 10, + freshness: Optional[str] = None, + offset: Optional[int] = None, + country: Optional[str] = None, + language: Optional[str] = None, + safesearch: Optional[str] = None, + livecrawl: Optional[str] = None, + livecrawl_formats: Optional[Iterable[str]] = 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, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.SearchResponse: + _warn("you.search.unified()", "you.search()") + + return self._you._search_impl( + query=query, + count=count, + freshness=freshness, + offset=offset, + country=country, + language=language, + safesearch=safesearch, + livecrawl=livecrawl, + livecrawl_formats=livecrawl_formats, + include_domains=_split_csv(include_domains), + exclude_domains=_split_csv(exclude_domains), + boost_domains=_split_csv(boost_domains), + crawl_timeout=crawl_timeout, + retries=retries, + server_url=server_url, + timeout_ms=timeout_ms, + http_headers=http_headers, + ) + + async def unified_async( + self, + *, + query: str, + count: Optional[int] = 10, + freshness: Optional[str] = None, + offset: Optional[int] = None, + country: Optional[str] = None, + language: Optional[str] = None, + safesearch: Optional[str] = None, + livecrawl: Optional[str] = None, + livecrawl_formats: Optional[Iterable[str]] = 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, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.SearchResponse: + _warn("you.search.unified_async()", "you.search_async()") + + return await self._you._search_async_impl( + query=query, + count=count, + freshness=freshness, + offset=offset, + country=country, + language=language, + safesearch=safesearch, + livecrawl=livecrawl, + livecrawl_formats=livecrawl_formats, + include_domains=_split_csv(include_domains), + exclude_domains=_split_csv(exclude_domains), + boost_domains=_split_csv(boost_domains), + crawl_timeout=crawl_timeout, + retries=retries, + server_url=server_url, + timeout_ms=timeout_ms, + http_headers=http_headers, + ) + + +class ContentsShim: + """Callable shim for ``you.contents``. + + ``you.contents(urls=...)`` → delegates to ``you._contents_impl()`` (no warning). + ``you.contents.generate(urls=...)`` → delegates with DeprecationWarning. + """ + + def __init__(self, you: Any) -> None: + self._you = you + + def __call__( + self, + *, + 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, + http_headers: Optional[Mapping[str, str]] = None, + ) -> List[models.ContentsResponse]: + return self._you._contents_impl( + urls=urls, + formats=formats, + crawl_timeout=crawl_timeout, + max_age=max_age, + retries=retries, + server_url=server_url, + timeout_ms=timeout_ms, + http_headers=http_headers, + ) + + def generate( + self, + *, + 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, + http_headers: Optional[Mapping[str, str]] = None, + ) -> List[models.ContentsResponse]: + _warn("you.contents.generate()", "you.contents()") + return self._you._contents_impl( + urls=urls, + formats=formats, + crawl_timeout=crawl_timeout, + max_age=max_age, + retries=retries, + server_url=server_url, + timeout_ms=timeout_ms, + http_headers=http_headers, + ) + + async def generate_async( + self, + *, + 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, + http_headers: Optional[Mapping[str, str]] = None, + ) -> List[models.ContentsResponse]: + _warn("you.contents.generate_async()", "you.contents_async()") + return await self._you._contents_async_impl( + urls=urls, + formats=formats, + crawl_timeout=crawl_timeout, + max_age=max_age, + retries=retries, + server_url=server_url, + timeout_ms=timeout_ms, + http_headers=http_headers, + ) diff --git a/src/youdotcom/_version.py b/src/youdotcom/_version.py index 3cd2c70..7e641ce 100644 --- a/src/youdotcom/_version.py +++ b/src/youdotcom/_version.py @@ -1,15 +1,14 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" import importlib.metadata __title__: str = "youdotcom" -__version__: str = "2.5.0" +__version__: str = "2.6.0" __openapi_doc_version__: str = "1.0.0" -__gen_version__: str = "2.918.1" -__user_agent__: str = "speakeasy-sdk/python 2.5.0 2.918.1 1.0.0 youdotcom" try: if __package__ is not None: __version__ = importlib.metadata.version(__package__) except importlib.metadata.PackageNotFoundError: pass + +__user_agent__: str = f"youdotcom-python-sdk/{__version__}" diff --git a/src/youdotcom/agents.py b/src/youdotcom/agents.py deleted file mode 100644 index 9090364..0000000 --- a/src/youdotcom/agents.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from .basesdk import BaseSDK -from .sdkconfiguration import SDKConfiguration -from typing import Optional -from youdotcom.runs import Runs - - -class Agents(BaseSDK): - runs: Runs - - def __init__( - self, sdk_config: SDKConfiguration, parent_ref: Optional[object] = None - ) -> None: - BaseSDK.__init__(self, sdk_config, parent_ref=parent_ref) - self.sdk_configuration = sdk_config - self._init_sdks() - - def _init_sdks(self): - self.runs = Runs(self.sdk_configuration, parent_ref=self.parent_ref) diff --git a/src/youdotcom/basesdk.py b/src/youdotcom/basesdk.py index 8665d7f..f2834a6 100644 --- a/src/youdotcom/basesdk.py +++ b/src/youdotcom/basesdk.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from .sdkconfiguration import SDKConfiguration import httpx @@ -18,6 +18,17 @@ run_sync_in_thread, ) +_SENSITIVE_HEADERS = {"authorization", "x-api-key", "cookie", "set-cookie"} + + +def _redact_headers(headers: httpx.Headers) -> httpx.Headers: + """Return a copy of headers with sensitive values redacted for debug logging.""" + redacted = httpx.Headers(headers) + for key in redacted.keys(): + if key.lower() in _SENSITIVE_HEADERS: + redacted[key] = "[REDACTED]" + return redacted + class BaseSDK: sdk_configuration: SDKConfiguration @@ -259,7 +270,7 @@ def do(): "Request:\nMethod: %s\nURL: %s\nHeaders: %s\nBody: %s", req.method, req.url, - req.headers, + _redact_headers(req.headers), get_body_content(req), ) @@ -281,7 +292,7 @@ def do(): "Response:\nStatus Code: %s\nURL: %s\nHeaders: %s\nBody: %s", http_res.status_code, http_res.url, - http_res.headers, + _redact_headers(http_res.headers), "" if stream else http_res.text, ) @@ -333,7 +344,7 @@ async def do(): "Request:\nMethod: %s\nURL: %s\nHeaders: %s\nBody: %s", req.method, req.url, - req.headers, + _redact_headers(req.headers), get_body_content(req), ) @@ -358,7 +369,7 @@ async def do(): "Response:\nStatus Code: %s\nURL: %s\nHeaders: %s\nBody: %s", http_res.status_code, http_res.url, - http_res.headers, + _redact_headers(http_res.headers), "" if stream else http_res.text, ) diff --git a/src/youdotcom/contents_sdk.py b/src/youdotcom/contents_sdk.py deleted file mode 100644 index e7ce613..0000000 --- a/src/youdotcom/contents_sdk.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from .basesdk import BaseSDK -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 -from youdotcom.utils import get_security_from_env -from youdotcom.utils.unmarshal_json_response import unmarshal_json_response - - -class ContentsSDK(BaseSDK): - def generate( - self, - *, - 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, - http_headers: Optional[Mapping[str, str]] = None, - ) -> List[models.ContentsResponse]: - r"""Returns the content of the web pages - - Returns the HTML or Markdown of a target webpage. - - :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 - :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.CONTENTS_OP_SERVERS[0] - - request = models.ContentsRequest( - 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( - method="POST", - path="/v1/contents", - 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.ContentsRequest - ), - 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="contents", - oauth2_scopes=None, - security_source=get_security_from_env( - self.sdk_configuration.security, models.Security - ), - tags=["contents"], - 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(List[models.ContentsResponse], http_res) - if utils.match_response(http_res, "401", "application/json"): - response_data = unmarshal_json_response( - errors.ContentsUnauthorizedErrorData, http_res - ) - raise errors.ContentsUnauthorizedError(response_data, http_res) - if utils.match_response(http_res, "403", "application/json"): - response_data = unmarshal_json_response( - errors.ContentsForbiddenErrorData, http_res - ) - raise errors.ContentsForbiddenError(response_data, http_res) - if utils.match_response(http_res, "500", "application/json"): - response_data = unmarshal_json_response( - errors.ContentsInternalServerErrorData, http_res - ) - raise errors.ContentsInternalServerError(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 generate_async( - self, - *, - 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, - http_headers: Optional[Mapping[str, str]] = None, - ) -> List[models.ContentsResponse]: - r"""Returns the content of the web pages - - Returns the HTML or Markdown of a target webpage. - - :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 - :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.CONTENTS_OP_SERVERS[0] - - request = models.ContentsRequest( - 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( - method="POST", - path="/v1/contents", - 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.ContentsRequest - ), - 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="contents", - oauth2_scopes=None, - security_source=get_security_from_env( - self.sdk_configuration.security, models.Security - ), - tags=["contents"], - 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(List[models.ContentsResponse], http_res) - if utils.match_response(http_res, "401", "application/json"): - response_data = unmarshal_json_response( - errors.ContentsUnauthorizedErrorData, http_res - ) - raise errors.ContentsUnauthorizedError(response_data, http_res) - if utils.match_response(http_res, "403", "application/json"): - response_data = unmarshal_json_response( - errors.ContentsForbiddenErrorData, http_res - ) - raise errors.ContentsForbiddenError(response_data, http_res) - if utils.match_response(http_res, "500", "application/json"): - response_data = unmarshal_json_response( - errors.ContentsInternalServerErrorData, http_res - ) - raise errors.ContentsInternalServerError(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/errors/__init__.py b/src/youdotcom/errors/__init__.py index aea2e79..0ad3b76 100644 --- a/src/youdotcom/errors/__init__.py +++ b/src/youdotcom/errors/__init__.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from .youerror import YouError from typing import Any, TYPE_CHECKING @@ -6,18 +6,6 @@ from youdotcom.utils.dynamic_imports import lazy_getattr, lazy_dir if TYPE_CHECKING: - from .agentruns400response_error import ( - AgentRuns400ResponseError, - AgentRuns400ResponseErrorData, - ) - from .agentruns401response_error import ( - AgentRuns401ResponseError, - AgentRuns401ResponseErrorData, - ) - from .agentruns422response_error import ( - AgentRuns422ResponseError, - AgentRuns422ResponseErrorData, - ) from .contentsop import ( ContentsForbiddenError, ContentsForbiddenErrorData, @@ -55,6 +43,10 @@ InternalServerErrorResponseData, ) from .no_response_error import NoResponseError + from .paymentrequired_response_error import ( + PaymentRequiredResponseError, + PaymentRequiredResponseErrorData, + ) from .researchop import ( ResearchForbiddenError, ResearchForbiddenErrorData, @@ -87,12 +79,6 @@ from .youdefaulterror import YouDefaultError __all__ = [ - "AgentRuns400ResponseError", - "AgentRuns400ResponseErrorData", - "AgentRuns401ResponseError", - "AgentRuns401ResponseErrorData", - "AgentRuns422ResponseError", - "AgentRuns422ResponseErrorData", "ContentsForbiddenError", "ContentsForbiddenErrorData", "ContentsInternalServerError", @@ -120,6 +106,8 @@ "InternalServerErrorResponse", "InternalServerErrorResponseData", "NoResponseError", + "PaymentRequiredResponseError", + "PaymentRequiredResponseErrorData", "ResearchForbiddenError", "ResearchForbiddenErrorData", "ResearchInternalServerError", @@ -146,12 +134,6 @@ ] _dynamic_imports: dict[str, str] = { - "AgentRuns400ResponseError": ".agentruns400response_error", - "AgentRuns400ResponseErrorData": ".agentruns400response_error", - "AgentRuns401ResponseError": ".agentruns401response_error", - "AgentRuns401ResponseErrorData": ".agentruns401response_error", - "AgentRuns422ResponseError": ".agentruns422response_error", - "AgentRuns422ResponseErrorData": ".agentruns422response_error", "ContentsForbiddenError": ".contentsop", "ContentsForbiddenErrorData": ".contentsop", "ContentsInternalServerError": ".contentsop", @@ -179,6 +161,8 @@ "InternalServerErrorResponse": ".internalservererror_response", "InternalServerErrorResponseData": ".internalservererror_response", "NoResponseError": ".no_response_error", + "PaymentRequiredResponseError": ".paymentrequired_response_error", + "PaymentRequiredResponseErrorData": ".paymentrequired_response_error", "ResearchForbiddenError": ".researchop", "ResearchForbiddenErrorData": ".researchop", "ResearchInternalServerError": ".researchop", diff --git a/src/youdotcom/errors/agentruns400response_error.py b/src/youdotcom/errors/agentruns400response_error.py deleted file mode 100644 index bce533b..0000000 --- a/src/youdotcom/errors/agentruns400response_error.py +++ /dev/null @@ -1,29 +0,0 @@ -"""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 AgentRuns400ResponseErrorData(BaseModel): - detail: Optional[str] = None - - -@dataclass(unsafe_hash=True) -class AgentRuns400ResponseError(YouError): - r"""The message returned by the error""" - - data: AgentRuns400ResponseErrorData = field(hash=False) - - def __init__( - self, - data: AgentRuns400ResponseErrorData, - 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/agentruns401response_error.py b/src/youdotcom/errors/agentruns401response_error.py deleted file mode 100644 index 50df678..0000000 --- a/src/youdotcom/errors/agentruns401response_error.py +++ /dev/null @@ -1,29 +0,0 @@ -"""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 AgentRuns401ResponseErrorData(BaseModel): - detail: Optional[str] = None - - -@dataclass(unsafe_hash=True) -class AgentRuns401ResponseError(YouError): - r"""The message returned by the error""" - - data: AgentRuns401ResponseErrorData = field(hash=False) - - def __init__( - self, - data: AgentRuns401ResponseErrorData, - 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/agentruns422response_error.py b/src/youdotcom/errors/agentruns422response_error.py deleted file mode 100644 index c86c94a..0000000 --- a/src/youdotcom/errors/agentruns422response_error.py +++ /dev/null @@ -1,30 +0,0 @@ -"""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 ( - agentruns422response_error as models_agentruns422response_error, -) -from youdotcom.types import BaseModel - - -class AgentRuns422ResponseErrorData(BaseModel): - detail: Optional[List[models_agentruns422response_error.Detail]] = None - - -@dataclass(unsafe_hash=True) -class AgentRuns422ResponseError(YouError): - data: AgentRuns422ResponseErrorData = field(hash=False) - - def __init__( - self, - data: AgentRuns422ResponseErrorData, - 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/contentsop.py b/src/youdotcom/errors/contentsop.py index 443fd30..3329e53 100644 --- a/src/youdotcom/errors/contentsop.py +++ b/src/youdotcom/errors/contentsop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/youdotcom/errors/finance_researchop.py b/src/youdotcom/errors/finance_researchop.py index a42f34d..2ce54e6 100644 --- a/src/youdotcom/errors/finance_researchop.py +++ b/src/youdotcom/errors/finance_researchop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/youdotcom/errors/forbidden_response_error.py b/src/youdotcom/errors/forbidden_response_error.py index 575dded..e40a871 100644 --- a/src/youdotcom/errors/forbidden_response_error.py +++ b/src/youdotcom/errors/forbidden_response_error.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/youdotcom/errors/getresearchtaskop.py b/src/youdotcom/errors/getresearchtaskop.py index 1c3cda1..4b5eac8 100644 --- a/src/youdotcom/errors/getresearchtaskop.py +++ b/src/youdotcom/errors/getresearchtaskop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/youdotcom/errors/internalservererror_response.py b/src/youdotcom/errors/internalservererror_response.py index 4a8c1a8..1ddc8ce 100644 --- a/src/youdotcom/errors/internalservererror_response.py +++ b/src/youdotcom/errors/internalservererror_response.py @@ -1,15 +1,27 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +"""Error model for HTTP 500 responses. + +Handles two possible 500 body shapes: + - ``{"detail": "..."}`` — plain detail string + - ``{"errors": [{"status": "500", "code": "...", "title": "...", ...}]}`` — + JSON:API format (returned by controller-level error handlers) + +Both fields are optional so either shape deserializes without crashing. +""" from __future__ import annotations from dataclasses import dataclass, field import httpx -from typing import Optional +from typing import Any, List, Optional from youdotcom.errors import YouError from youdotcom.types import BaseModel class InternalServerErrorResponseData(BaseModel): detail: Optional[str] = None + r"""A description of the error.""" + + errors: Optional[List[dict[str, Any]]] = None + r"""JSON:API error array from controller-level error handlers.""" @dataclass(unsafe_hash=True) diff --git a/src/youdotcom/errors/no_response_error.py b/src/youdotcom/errors/no_response_error.py index 1deab64..f72947a 100644 --- a/src/youdotcom/errors/no_response_error.py +++ b/src/youdotcom/errors/no_response_error.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from dataclasses import dataclass diff --git a/src/youdotcom/errors/paymentrequired_response_error.py b/src/youdotcom/errors/paymentrequired_response_error.py new file mode 100644 index 0000000..08d1a2b --- /dev/null +++ b/src/youdotcom/errors/paymentrequired_response_error.py @@ -0,0 +1,43 @@ +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 PaymentRequiredResponseErrorData(BaseModel): + r"""Body of a 402 response — returned when the account + cannot make paid API requests (free-tier limit exceeded, insufficient credits). + """ + error: Optional[str] = None + r"""The error code (e.g. ``"payment_required"``).""" + message: Optional[str] = None + r"""A human-readable description of the error.""" + upgrade_url: Optional[str] = None + r"""URL for adding credits or upgrading the account.""" + limit: Optional[int] = None + r"""The usage limit, when available.""" + used: Optional[int] = None + r"""The usage consumed, when available.""" + period: Optional[str] = None + r"""The usage period, when available.""" + reset_at: Optional[str] = None + r"""The reset timestamp, when available.""" + + +@dataclass(unsafe_hash=True) +class PaymentRequiredResponseError(YouError): + r"""Payment Required (402). The account cannot make paid API requests.""" + + data: PaymentRequiredResponseErrorData = field(hash=False) + + def __init__( + self, + data: PaymentRequiredResponseErrorData, + 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 a64bebb..e8bfa98 100644 --- a/src/youdotcom/errors/researchop.py +++ b/src/youdotcom/errors/researchop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/youdotcom/errors/responsevalidationerror.py b/src/youdotcom/errors/responsevalidationerror.py index 8e3bb21..f92fdda 100644 --- a/src/youdotcom/errors/responsevalidationerror.py +++ b/src/youdotcom/errors/responsevalidationerror.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import httpx from typing import Optional diff --git a/src/youdotcom/errors/streamresearchtaskop.py b/src/youdotcom/errors/streamresearchtaskop.py index a2ee4a0..c6e64dd 100644 --- a/src/youdotcom/errors/streamresearchtaskop.py +++ b/src/youdotcom/errors/streamresearchtaskop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/youdotcom/errors/unauthorized_response_error.py b/src/youdotcom/errors/unauthorized_response_error.py index dbc2f9f..82c2081 100644 --- a/src/youdotcom/errors/unauthorized_response_error.py +++ b/src/youdotcom/errors/unauthorized_response_error.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/src/youdotcom/errors/unprocessableentity_response_error.py b/src/youdotcom/errors/unprocessableentity_response_error.py index f915365..f7696c2 100644 --- a/src/youdotcom/errors/unprocessableentity_response_error.py +++ b/src/youdotcom/errors/unprocessableentity_response_error.py @@ -1,15 +1,34 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +"""Error model for HTTP 422 responses. + +Handles three possible 422 body shapes returned across You.com endpoints: + - ``{"error": "..."}`` — search spec format + - ``{"detail": [{"type": "...", "loc": [...], "msg": "...", ...}]}`` — FastAPI + request validation errors (returned before handler runs) + - ``{"errors": [{"status": "422", "code": "...", "title": "...", ...}]}`` — + JSON:API format (returned by controller-level error handlers) + +All fields are optional so any shape deserializes without crashing. The raw +response is always preserved on the error object for callers that need the +full body. +""" from __future__ import annotations from dataclasses import dataclass, field import httpx -from typing import Optional +from typing import Any, List, Optional from youdotcom.errors import YouError from youdotcom.types import BaseModel class UnprocessableEntityResponseErrorData(BaseModel): error: Optional[str] = None + r"""Error code from the search spec 422 format.""" + + detail: Optional[List[dict[str, Any]]] = None + r"""Validation error array from FastAPI's RequestValidationError.""" + + errors: Optional[List[dict[str, Any]]] = None + r"""JSON:API error array from controller-level error handlers.""" @dataclass(unsafe_hash=True) diff --git a/src/youdotcom/errors/youdefaulterror.py b/src/youdotcom/errors/youdefaulterror.py index 795cc56..9bc1e8e 100644 --- a/src/youdotcom/errors/youdefaulterror.py +++ b/src/youdotcom/errors/youdefaulterror.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import httpx from typing import Optional diff --git a/src/youdotcom/errors/youerror.py b/src/youdotcom/errors/youerror.py index cedd8c0..56e1179 100644 --- a/src/youdotcom/errors/youerror.py +++ b/src/youdotcom/errors/youerror.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import httpx from typing import Optional diff --git a/src/youdotcom/httpclient.py b/src/youdotcom/httpclient.py index 89560b5..e0ea6aa 100644 --- a/src/youdotcom/httpclient.py +++ b/src/youdotcom/httpclient.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + # pyright: reportReturnType = false import asyncio diff --git a/src/youdotcom/models/__init__.py b/src/youdotcom/models/__init__.py index ad650a3..8c74c94 100644 --- a/src/youdotcom/models/__init__.py +++ b/src/youdotcom/models/__init__.py @@ -1,49 +1,14 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from typing import Any, TYPE_CHECKING from youdotcom.utils.dynamic_imports import lazy_getattr, lazy_dir if TYPE_CHECKING: - from .advancedagentrunsrequest import ( - AdvancedAgentRunsRequest, - AdvancedAgentRunsRequestTypedDict, - Tool, - ToolTypedDict, - WorkflowConfig, - WorkflowConfigTypedDict, - ) - from .agentruns422response_error import Detail, DetailTypedDict, Loc, LocTypedDict - from .agentrunsbatchresponse import ( - AgentRunsBatchResponse, - AgentRunsBatchResponseTypedDict, - Input, - InputTypedDict, - Role, - ) - from .agentrunsresponseoutput import ( - AgentRunsResponseOutput, - AgentRunsResponseOutputTypedDict, - Type, - ) - from .agentrunsresponsewebsearchresult import ( - AgentRunsResponseWebSearchResult, - AgentRunsResponseWebSearchResultTypedDict, - ) - from .agentrunsstreamingresponse import ( - AgentRunsStreamingResponse, - AgentRunsStreamingResponseTypedDict, - Data, - DataTypedDict, - ) - from .agentsrunsop import ( - AGENTS_RUNS_OP_SERVERS, - AgentsRunsRequest, - AgentsRunsRequestTypedDict, - AgentsRunsResponse, - AgentsRunsResponseTypedDict, - ) - from .computetool import ComputeTool, ComputeToolTypedDict + from .answercitation import AnswerCitation + from .answerrequestbody import AnswerRequestBody + from .answerresponse import AnswerResponse, AnswerResults + from .answersearchresult import AnswerSearchResult from .contents import Contents, ContentsTypedDict from .contentsformats import ContentsFormats from .contentsmetadata import ContentsMetadata, ContentsMetadataTypedDict @@ -55,14 +20,6 @@ ContentsResponseTypedDict, ) from .country import Country - from .customagentrunsrequest import ( - CustomAgentRunsRequest, - CustomAgentRunsRequestTypedDict, - ) - from .expressagentrunsrequest import ( - ExpressAgentRunsRequest, - ExpressAgentRunsRequestTypedDict, - ) from .finance_researchop import ( FinanceResearchContentType, FinanceResearchDetail, @@ -93,7 +50,6 @@ from .livecrawl import LiveCrawl from .livecrawlformats import LiveCrawlFormats from .newsresult import NewsResult, NewsResultTypedDict - from .reportverbosity import ReportVerbosity from .researcheffort import ResearchEffort from .researchop import ( ResearchDetail, @@ -129,44 +85,9 @@ ResearchTaskStreamEventDataTypedDict, ResearchTaskStreamEventTypedDict, ) - from .researchtool import ResearchTool, ResearchToolTypedDict - from .response_created import ResponseCreated, ResponseCreatedTypedDict - from .response_done import ( - ResponseDone, - ResponseDoneResponse, - ResponseDoneResponseTypedDict, - ResponseDoneTypedDict, - ) - from .response_output_content_full import ( - ResponseOutputContentFull, - ResponseOutputContentFullResponse, - ResponseOutputContentFullResponseTypedDict, - ResponseOutputContentFullTypedDict, - ) - from .response_output_item_added import ( - ResponseOutputItemAdded, - ResponseOutputItemAddedResponse, - ResponseOutputItemAddedResponseTypedDict, - ResponseOutputItemAddedTypedDict, - ) - from .response_output_item_done import ( - ResponseOutputItemDone, - ResponseOutputItemDoneResponse, - ResponseOutputItemDoneResponseTypedDict, - ResponseOutputItemDoneTypedDict, - ) - from .response_output_text_delta import ( - ResponseOutputTextDelta, - ResponseOutputTextDeltaResponse, - ResponseOutputTextDeltaResponseTypedDict, - ResponseOutputTextDeltaTypedDict, - ) - from .response_starting import ResponseStarting, ResponseStartingTypedDict from .safesearch import SafeSearch - from .searcheffort import SearchEffort 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, @@ -189,29 +110,15 @@ 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", - "AgentRunsBatchResponseTypedDict", - "AgentRunsResponseOutput", - "AgentRunsResponseOutputTypedDict", - "AgentRunsResponseWebSearchResult", - "AgentRunsResponseWebSearchResultTypedDict", - "AgentRunsStreamingResponse", - "AgentRunsStreamingResponseTypedDict", - "AgentsRunsRequest", - "AgentsRunsRequestTypedDict", - "AgentsRunsResponse", - "AgentsRunsResponseTypedDict", + "AnswerCitation", + "AnswerRequestBody", + "AnswerResponse", + "AnswerResults", + "AnswerSearchResult", "CONTENTS_OP_SERVERS", - "ComputeTool", - "ComputeToolTypedDict", "Content", "ContentType", "ContentTypedDict", @@ -225,15 +132,7 @@ "ContentsResponseTypedDict", "ContentsTypedDict", "Country", - "CustomAgentRunsRequest", - "CustomAgentRunsRequestTypedDict", - "Data", - "DataTypedDict", - "Detail", - "DetailTypedDict", "Event", - "ExpressAgentRunsRequest", - "ExpressAgentRunsRequestTypedDict", "FinanceResearchContentType", "FinanceResearchDetail", "FinanceResearchDetailTypedDict", @@ -257,18 +156,13 @@ "FreshnessValueTypedDict", "GetResearchTaskRequest", "GetResearchTaskRequestTypedDict", - "Input", - "InputTypedDict", "Language", "LiveCrawl", "LiveCrawlFormats", - "Loc", - "LocTypedDict", "NewsResult", "NewsResultTypedDict", "Output", "OutputTypedDict", - "ReportVerbosity", "ResearchDetail", "ResearchDetailTypedDict", "ResearchEffort", @@ -288,41 +182,12 @@ "ResearchTaskStreamEventData", "ResearchTaskStreamEventDataTypedDict", "ResearchTaskStreamEventTypedDict", - "ResearchTool", - "ResearchToolTypedDict", - "ResponseCreated", - "ResponseCreatedTypedDict", - "ResponseDone", - "ResponseDoneResponse", - "ResponseDoneResponseTypedDict", - "ResponseDoneTypedDict", - "ResponseOutputContentFull", - "ResponseOutputContentFullResponse", - "ResponseOutputContentFullResponseTypedDict", - "ResponseOutputContentFullTypedDict", - "ResponseOutputItemAdded", - "ResponseOutputItemAddedResponse", - "ResponseOutputItemAddedResponseTypedDict", - "ResponseOutputItemAddedTypedDict", - "ResponseOutputItemDone", - "ResponseOutputItemDoneResponse", - "ResponseOutputItemDoneResponseTypedDict", - "ResponseOutputItemDoneTypedDict", - "ResponseOutputTextDelta", - "ResponseOutputTextDeltaResponse", - "ResponseOutputTextDeltaResponseTypedDict", - "ResponseOutputTextDeltaTypedDict", - "ResponseStarting", - "ResponseStartingTypedDict", "Result", "ResultTypedDict", "Results", "ResultsTypedDict", - "Role", "SEARCH_OP_SERVERS", - "SEARCH_POST_OP_SERVERS", "SafeSearch", - "SearchEffort", "SearchMetadata", "SearchMetadataTypedDict", "SearchRequest", @@ -347,50 +212,16 @@ "TaskResponse", "TaskResponseStatus", "TaskResponseTypedDict", - "Tool", - "ToolTypedDict", - "Type", - "Verbosity", "WebResult", "WebResultTypedDict", - "WebSearchTool", - "WebSearchToolTypedDict", - "WorkflowConfig", - "WorkflowConfigTypedDict", ] _dynamic_imports: dict[str, str] = { - "AdvancedAgentRunsRequest": ".advancedagentrunsrequest", - "AdvancedAgentRunsRequestTypedDict": ".advancedagentrunsrequest", - "Tool": ".advancedagentrunsrequest", - "ToolTypedDict": ".advancedagentrunsrequest", - "WorkflowConfig": ".advancedagentrunsrequest", - "WorkflowConfigTypedDict": ".advancedagentrunsrequest", - "Detail": ".agentruns422response_error", - "DetailTypedDict": ".agentruns422response_error", - "Loc": ".agentruns422response_error", - "LocTypedDict": ".agentruns422response_error", - "AgentRunsBatchResponse": ".agentrunsbatchresponse", - "AgentRunsBatchResponseTypedDict": ".agentrunsbatchresponse", - "Input": ".agentrunsbatchresponse", - "InputTypedDict": ".agentrunsbatchresponse", - "Role": ".agentrunsbatchresponse", - "AgentRunsResponseOutput": ".agentrunsresponseoutput", - "AgentRunsResponseOutputTypedDict": ".agentrunsresponseoutput", - "Type": ".agentrunsresponseoutput", - "AgentRunsResponseWebSearchResult": ".agentrunsresponsewebsearchresult", - "AgentRunsResponseWebSearchResultTypedDict": ".agentrunsresponsewebsearchresult", - "AgentRunsStreamingResponse": ".agentrunsstreamingresponse", - "AgentRunsStreamingResponseTypedDict": ".agentrunsstreamingresponse", - "Data": ".agentrunsstreamingresponse", - "DataTypedDict": ".agentrunsstreamingresponse", - "AGENTS_RUNS_OP_SERVERS": ".agentsrunsop", - "AgentsRunsRequest": ".agentsrunsop", - "AgentsRunsRequestTypedDict": ".agentsrunsop", - "AgentsRunsResponse": ".agentsrunsop", - "AgentsRunsResponseTypedDict": ".agentsrunsop", - "ComputeTool": ".computetool", - "ComputeToolTypedDict": ".computetool", + "AnswerCitation": ".answercitation", + "AnswerRequestBody": ".answerrequestbody", + "AnswerResponse": ".answerresponse", + "AnswerResults": ".answerresponse", + "AnswerSearchResult": ".answersearchresult", "Contents": ".contents", "ContentsTypedDict": ".contents", "ContentsFormats": ".contentsformats", @@ -402,10 +233,6 @@ "ContentsResponse": ".contentsop", "ContentsResponseTypedDict": ".contentsop", "Country": ".country", - "CustomAgentRunsRequest": ".customagentrunsrequest", - "CustomAgentRunsRequestTypedDict": ".customagentrunsrequest", - "ExpressAgentRunsRequest": ".expressagentrunsrequest", - "ExpressAgentRunsRequestTypedDict": ".expressagentrunsrequest", "FinanceResearchContentType": ".finance_researchop", "FinanceResearchDetail": ".finance_researchop", "FinanceResearchDetailTypedDict": ".finance_researchop", @@ -434,7 +261,6 @@ "LiveCrawlFormats": ".livecrawlformats", "NewsResult": ".newsresult", "NewsResultTypedDict": ".newsresult", - "ReportVerbosity": ".reportverbosity", "ResearchEffort": ".researcheffort", "ResearchDetail": ".researchop", "ResearchDetailTypedDict": ".researchop", @@ -464,40 +290,12 @@ "ResearchTaskStreamEventData": ".researchtaskstreamevent", "ResearchTaskStreamEventDataTypedDict": ".researchtaskstreamevent", "ResearchTaskStreamEventTypedDict": ".researchtaskstreamevent", - "ResearchTool": ".researchtool", - "ResearchToolTypedDict": ".researchtool", - "ResponseCreated": ".response_created", - "ResponseCreatedTypedDict": ".response_created", - "ResponseDone": ".response_done", - "ResponseDoneResponse": ".response_done", - "ResponseDoneResponseTypedDict": ".response_done", - "ResponseDoneTypedDict": ".response_done", - "ResponseOutputContentFull": ".response_output_content_full", - "ResponseOutputContentFullResponse": ".response_output_content_full", - "ResponseOutputContentFullResponseTypedDict": ".response_output_content_full", - "ResponseOutputContentFullTypedDict": ".response_output_content_full", - "ResponseOutputItemAdded": ".response_output_item_added", - "ResponseOutputItemAddedResponse": ".response_output_item_added", - "ResponseOutputItemAddedResponseTypedDict": ".response_output_item_added", - "ResponseOutputItemAddedTypedDict": ".response_output_item_added", - "ResponseOutputItemDone": ".response_output_item_done", - "ResponseOutputItemDoneResponse": ".response_output_item_done", - "ResponseOutputItemDoneResponseTypedDict": ".response_output_item_done", - "ResponseOutputItemDoneTypedDict": ".response_output_item_done", - "ResponseOutputTextDelta": ".response_output_text_delta", - "ResponseOutputTextDeltaResponse": ".response_output_text_delta", - "ResponseOutputTextDeltaResponseTypedDict": ".response_output_text_delta", - "ResponseOutputTextDeltaTypedDict": ".response_output_text_delta", - "ResponseStarting": ".response_starting", - "ResponseStartingTypedDict": ".response_starting", "SafeSearch": ".safesearch", - "SearchEffort": ".searcheffort", "SearchMetadata": ".searchmetadata", "SearchMetadataTypedDict": ".searchmetadata", "SEARCH_OP_SERVERS": ".searchop", "SearchRequest": ".searchop", "SearchRequestTypedDict": ".searchop", - "SEARCH_POST_OP_SERVERS": ".searchpostop", "SearchRequestBody": ".searchrequestbody", "SearchRequestBodyTypedDict": ".searchrequestbody", "Results": ".searchresponse", @@ -518,11 +316,8 @@ "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 deleted file mode 100644 index 8511c0a..0000000 --- a/src/youdotcom/models/advancedagentrunsrequest.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from .computetool import ComputeTool, ComputeToolTypedDict -from .researchtool import ResearchTool, ResearchToolTypedDict -from .verbosity import Verbosity -import pydantic -from pydantic import Field, model_serializer -from pydantic.functional_validators import AfterValidator -from typing import List, Literal, Optional, Union -from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict -from youdotcom.types import BaseModel, UNSET_SENTINEL -from youdotcom.utils import validate_const - - -ToolTypedDict = TypeAliasType( - "ToolTypedDict", Union[ComputeToolTypedDict, ResearchToolTypedDict] -) - - -Tool = Annotated[Union[ComputeTool, ResearchTool], Field(discriminator="TYPE")] - - -class WorkflowConfigTypedDict(TypedDict): - r"""Defines the maximum number of steps the agent uses in its workflow plan to answer your query. Higher values allow for more tool calls, but it takes longer for the agent to provide the response. For instance, setting max_workflow_steps=5 could allow the agent to call the research tool 3 times and the compute tool 2 times.""" - - max_workflow_steps: NotRequired[int] - - -class WorkflowConfig(BaseModel): - r"""Defines the maximum number of steps the agent uses in its workflow plan to answer your query. Higher values allow for more tool calls, but it takes longer for the agent to provide the response. For instance, setting max_workflow_steps=5 could allow the agent to call the research tool 3 times and the compute tool 2 times.""" - - max_workflow_steps: Optional[int] = 10 - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["max_workflow_steps"]) - 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 AdvancedAgentRunsRequestTypedDict(TypedDict): - input: str - r"""The question you'd like to ask the agent""" - agent: Literal["advanced"] - r"""Setting this value to \"advanced\" is mandatory to use the advanced agent.""" - stream: NotRequired[bool] - r"""Must be set to `true` when you want to stream the agent response as it's being generated, and `false` when you want the response to return after the agent has finished.""" - tools: NotRequired[List[ToolTypedDict]] - r"""The advanced agent accepts either `compute` or `research` tools Compute allows your agent to use a Python code interpreter for tasks such as data analysis, mathematical calculations, and plot generation.

Research iteratively searches the web, analyzes the results, and stops when finished. It then provides a comprehensive report to your agent with current, cited information.
""" - verbosity: NotRequired[Verbosity] - r"""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.""" - workflow_config: NotRequired[WorkflowConfigTypedDict] - r"""Defines the maximum number of steps the agent uses in its workflow plan to answer your query. Higher values allow for more tool calls, but it takes longer for the agent to provide the response. For instance, setting max_workflow_steps=5 could allow the agent to call the research tool 3 times and the compute tool 2 times.""" - - -class AdvancedAgentRunsRequest(BaseModel): - input: str - r"""The question you'd like to ask the agent""" - - AGENT: Annotated[ - Annotated[Literal["advanced"], AfterValidator(validate_const("advanced"))], - pydantic.Field(alias="agent"), - ] = "advanced" - r"""Setting this value to \"advanced\" is mandatory to use the advanced agent.""" - - stream: Optional[bool] = False - r"""Must be set to `true` when you want to stream the agent response as it's being generated, and `false` when you want the response to return after the agent has finished.""" - - tools: Optional[List[Tool]] = None - r"""The advanced agent accepts either `compute` or `research` tools Compute allows your agent to use a Python code interpreter for tasks such as data analysis, mathematical calculations, and plot generation.

Research iteratively searches the web, analyzes the results, and stops when finished. It then provides a comprehensive report to your agent with current, cited information.
""" - - verbosity: Optional[Verbosity] = None - r"""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.""" - - workflow_config: Optional[WorkflowConfig] = None - r"""Defines the maximum number of steps the agent uses in its workflow plan to answer your query. Higher values allow for more tool calls, but it takes longer for the agent to provide the response. For instance, setting max_workflow_steps=5 could allow the agent to call the research tool 3 times and the compute tool 2 times.""" - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["stream", "tools", "verbosity", "workflow_config"]) - 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 - - -try: - AdvancedAgentRunsRequest.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/agentruns422response_error.py b/src/youdotcom/models/agentruns422response_error.py deleted file mode 100644 index 4124056..0000000 --- a/src/youdotcom/models/agentruns422response_error.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from typing import List, Union -from typing_extensions import TypeAliasType, TypedDict -from youdotcom.types import BaseModel - - -LocTypedDict = TypeAliasType("LocTypedDict", Union[str, int]) - - -Loc = TypeAliasType("Loc", Union[str, int]) - - -class DetailTypedDict(TypedDict): - type: str - loc: List[LocTypedDict] - msg: str - input: str - - -class Detail(BaseModel): - type: str - - loc: List[Loc] - - msg: str - - input: str diff --git a/src/youdotcom/models/agentrunsbatchresponse.py b/src/youdotcom/models/agentrunsbatchresponse.py deleted file mode 100644 index df8614a..0000000 --- a/src/youdotcom/models/agentrunsbatchresponse.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from .agentrunsresponseoutput import ( - AgentRunsResponseOutput, - AgentRunsResponseOutputTypedDict, -) -from enum import Enum -from pydantic import model_serializer -from typing import List, Optional -from typing_extensions import NotRequired, TypedDict -from youdotcom.types import BaseModel, UNSET_SENTINEL - - -class Role(str, Enum): - r"""The access based role of the user""" - - USER = "user" - - -class InputTypedDict(TypedDict): - role: Role - r"""The access based role of the user""" - content: str - r"""The question populated in the request payload""" - - -class Input(BaseModel): - role: Role - r"""The access based role of the user""" - - content: str - r"""The question populated in the request payload""" - - -class AgentRunsBatchResponseTypedDict(TypedDict): - agent: str - r"""The id of the agent populated in the request.""" - input: List[InputTypedDict] - r"""The users access role and question you asked the agent""" - output: List[AgentRunsResponseOutputTypedDict] - r"""Array of response outputs from the agent""" - mode: NotRequired[str] - r"""The mode of the agent""" - - -class AgentRunsBatchResponse(BaseModel): - agent: str - r"""The id of the agent populated in the request.""" - - input: List[Input] - r"""The users access role and question you asked the agent""" - - output: List[AgentRunsResponseOutput] - r"""Array of response outputs from the agent""" - - mode: Optional[str] = None - r"""The mode of the agent""" - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["mode"]) - 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/agentrunsresponseoutput.py b/src/youdotcom/models/agentrunsresponseoutput.py deleted file mode 100644 index 1eaa631..0000000 --- a/src/youdotcom/models/agentrunsresponseoutput.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from .agentrunsresponsewebsearchresult import ( - AgentRunsResponseWebSearchResult, - AgentRunsResponseWebSearchResultTypedDict, -) -from enum import Enum -from pydantic import model_serializer -from typing import List, Optional -from typing_extensions import NotRequired, TypedDict -from youdotcom.types import BaseModel, UNSET_SENTINEL - - -class Type(str, Enum): - r"""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 - """ - - MESSAGE_ANSWER = "message.answer" - WEB_SEARCH_RESULTS = "web_search.results" - - -class AgentRunsResponseOutputTypedDict(TypedDict): - r"""The response populated by the agent.""" - - type: Type - r"""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 - """ - text: NotRequired[str] - r"""The text response of the agent. This field returns when `type == message.answer`. The response returns as markdown formatted text. - - For an overview of Markdown syntax, see the [Basic Syntax Markdown Guide](https://www.markdownguide.org/basic-syntax/) - """ - content: NotRequired[List[AgentRunsResponseWebSearchResultTypedDict]] - r"""The text response of the agent. - This field returns when `type == web_search.results` - """ - - -class AgentRunsResponseOutput(BaseModel): - r"""The response populated by the agent.""" - - type: Type - r"""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 - """ - - text: Optional[str] = None - r"""The text response of the agent. This field returns when `type == message.answer`. The response returns as markdown formatted text. - - For an overview of Markdown syntax, see the [Basic Syntax Markdown Guide](https://www.markdownguide.org/basic-syntax/) - """ - - content: Optional[List[AgentRunsResponseWebSearchResult]] = None - r"""The text response of the agent. - This field returns when `type == web_search.results` - """ - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["text", "content"]) - 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/agentrunsresponsewebsearchresult.py b/src/youdotcom/models/agentrunsresponsewebsearchresult.py deleted file mode 100644 index 7f9d3c3..0000000 --- a/src/youdotcom/models/agentrunsresponsewebsearchresult.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic import model_serializer -from pydantic.functional_validators import AfterValidator -from typing import Literal, Optional -from typing_extensions import Annotated, NotRequired, TypedDict -from youdotcom.types import BaseModel, UNSET_SENTINEL -from youdotcom.utils import validate_const - - -class AgentRunsResponseWebSearchResultTypedDict(TypedDict): - r"""The text response of the agent. This field only returns when the type is `web_search.results`""" - - citation_uri: str - r"""The web search result the agent returned along in its response""" - title: str - r"""The title of the web site returned under url""" - snippet: str - r"""A textual portion of the web site returned under url""" - url: str - r"""The web search result the agent returned along in its response""" - source_type: Literal["web_search"] - r"""The type of content the agent can return outside a text response""" - provider: NotRequired[str] - r"""This is currently unused""" - thumbnail_url: NotRequired[str] - r"""The thumbnail image of the url""" - - -class AgentRunsResponseWebSearchResult(BaseModel): - r"""The text response of the agent. This field only returns when the type is `web_search.results`""" - - citation_uri: str - r"""The web search result the agent returned along in its response""" - - title: str - r"""The title of the web site returned under url""" - - snippet: str - r"""A textual portion of the web site returned under url""" - - url: str - r"""The web search result the agent returned along in its response""" - - SOURCE_TYPE: Annotated[ - Annotated[Literal["web_search"], AfterValidator(validate_const("web_search"))], - pydantic.Field(alias="source_type"), - ] = "web_search" - r"""The type of content the agent can return outside a text response""" - - provider: Optional[str] = None - r"""This is currently unused""" - - thumbnail_url: Optional[str] = None - r"""The thumbnail image of the url""" - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["provider", "thumbnail_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 - - -try: - AgentRunsResponseWebSearchResult.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/agentrunsstreamingresponse.py b/src/youdotcom/models/agentrunsstreamingresponse.py deleted file mode 100644 index 1a2c119..0000000 --- a/src/youdotcom/models/agentrunsstreamingresponse.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from .response_created import ResponseCreated, ResponseCreatedTypedDict -from .response_done import ResponseDone, ResponseDoneTypedDict -from .response_output_content_full import ( - ResponseOutputContentFull, - ResponseOutputContentFullTypedDict, -) -from .response_output_item_added import ( - ResponseOutputItemAdded, - ResponseOutputItemAddedTypedDict, -) -from .response_output_item_done import ( - ResponseOutputItemDone, - ResponseOutputItemDoneTypedDict, -) -from .response_output_text_delta import ( - ResponseOutputTextDelta, - ResponseOutputTextDeltaTypedDict, -) -from .response_starting import ResponseStarting, ResponseStartingTypedDict -from pydantic import Field -from typing import Union -from typing_extensions import Annotated, TypeAliasType, TypedDict -from youdotcom.types import BaseModel - - -DataTypedDict = TypeAliasType( - "DataTypedDict", - Union[ - ResponseCreatedTypedDict, - ResponseStartingTypedDict, - ResponseOutputItemAddedTypedDict, - ResponseOutputContentFullTypedDict, - ResponseOutputItemDoneTypedDict, - ResponseOutputTextDeltaTypedDict, - ResponseDoneTypedDict, - ], -) - - -Data = Annotated[ - Union[ - ResponseCreated, - ResponseStarting, - ResponseOutputItemAdded, - ResponseOutputContentFull, - ResponseOutputItemDone, - ResponseOutputTextDelta, - ResponseDone, - ], - Field(discriminator="TYPE"), -] - - -class AgentRunsStreamingResponseTypedDict(TypedDict): - r"""A server-sent event containing stock market update content""" - - id: str - r"""Sequence number of the SSE event, starts from 0""" - event: str - r"""The type of the SSE event""" - data: DataTypedDict - - -class AgentRunsStreamingResponse(BaseModel): - r"""A server-sent event containing stock market update content""" - - id: str - r"""Sequence number of the SSE event, starts from 0""" - - event: str - r"""The type of the SSE event""" - - data: Data diff --git a/src/youdotcom/models/agentsrunsop.py b/src/youdotcom/models/agentsrunsop.py deleted file mode 100644 index 52a57d5..0000000 --- a/src/youdotcom/models/agentsrunsop.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from .advancedagentrunsrequest import ( - AdvancedAgentRunsRequest, - AdvancedAgentRunsRequestTypedDict, -) -from .agentrunsbatchresponse import ( - AgentRunsBatchResponse, - AgentRunsBatchResponseTypedDict, -) -from .agentrunsstreamingresponse import ( - AgentRunsStreamingResponse, - AgentRunsStreamingResponseTypedDict, -) -from .customagentrunsrequest import ( - CustomAgentRunsRequest, - CustomAgentRunsRequestTypedDict, -) -from .expressagentrunsrequest import ( - ExpressAgentRunsRequest, - ExpressAgentRunsRequestTypedDict, -) -from typing import Union -from typing_extensions import TypeAliasType -from youdotcom.utils import eventstreaming - - -AGENTS_RUNS_OP_SERVERS = [ - "https://api.you.com", -] - - -AgentsRunsRequestTypedDict = TypeAliasType( - "AgentsRunsRequestTypedDict", - Union[ - CustomAgentRunsRequestTypedDict, - ExpressAgentRunsRequestTypedDict, - AdvancedAgentRunsRequestTypedDict, - ], -) -r"""The parameters to ask the agent a question""" - - -AgentsRunsRequest = TypeAliasType( - "AgentsRunsRequest", - Union[CustomAgentRunsRequest, ExpressAgentRunsRequest, AdvancedAgentRunsRequest], -) -r"""The parameters to ask the agent a question""" - - -AgentsRunsResponseTypedDict = TypeAliasType( - "AgentsRunsResponseTypedDict", - Union[ - AgentRunsBatchResponseTypedDict, - Union[ - eventstreaming.EventStream[AgentRunsStreamingResponseTypedDict], - eventstreaming.EventStreamAsync[AgentRunsStreamingResponseTypedDict], - ], - ], -) - - -AgentsRunsResponse = TypeAliasType( - "AgentsRunsResponse", - Union[ - AgentRunsBatchResponse, - Union[ - eventstreaming.EventStream[AgentRunsStreamingResponse], - eventstreaming.EventStreamAsync[AgentRunsStreamingResponse], - ], - ], -) diff --git a/src/youdotcom/models/answercitation.py b/src/youdotcom/models/answercitation.py new file mode 100644 index 0000000..2cee1cd --- /dev/null +++ b/src/youdotcom/models/answercitation.py @@ -0,0 +1,13 @@ +from __future__ import annotations +from typing import List, Optional +from youdotcom.types import BaseModel + + +class AnswerCitation(BaseModel): + r"""A source cited in the answer, with supporting excerpts.""" + + source: str + r"""The URL of the cited source.""" + + excerpts: Optional[List[str]] = None + r"""Verbatim excerpts from the cited source that support the answer.""" diff --git a/src/youdotcom/models/answerrequestbody.py b/src/youdotcom/models/answerrequestbody.py new file mode 100644 index 0000000..90a16c6 --- /dev/null +++ b/src/youdotcom/models/answerrequestbody.py @@ -0,0 +1,50 @@ +from __future__ import annotations +from .country import Country +from .freshnessvalue import FreshnessValue +from .language import Language +from pydantic import model_serializer +from typing import List, Optional +from youdotcom.types import BaseModel, UNSET_SENTINEL + + +class AnswerRequestBody(BaseModel): + r"""Request body for ``POST /v1/answer``.""" + + query: str + r"""The search query used to retrieve relevant web results. Max 400 characters. Search operators (``site:``, ``OR``, etc.) are not supported.""" + + freshness: Optional[FreshnessValue] = None + r"""Specifies the freshness of the results. One of ``day``, ``week``, ``month``, ``year``, or ``YYYY-MM-DDtoYYYY-MM-DD``.""" + + country: Optional[Country] = None + r"""A supported country code that determines the geographical focus of the web results.""" + + language: Optional[Language] = None + r"""A supported BCP 47 language tag that determines the language of the web results.""" + + include_domains: Optional[List[str]] = None + r"""Domains to exclusively include. Cannot combine with ``exclude_domains`` or ``boost_domains``. Max 500.""" + + exclude_domains: Optional[List[str]] = None + r"""Domains to exclude. Cannot combine with ``include_domains``. Can combine with ``boost_domains``. Max 500.""" + + boost_domains: Optional[List[str]] = None + r"""Domains to prefer in ranking. Cannot combine with ``include_domains``. Can combine with ``exclude_domains``. Max 500.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + ["freshness", "country", "language", "include_domains", "exclude_domains", "boost_domains"] + ) + 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/answerresponse.py b/src/youdotcom/models/answerresponse.py new file mode 100644 index 0000000..2b67887 --- /dev/null +++ b/src/youdotcom/models/answerresponse.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from .answercitation import AnswerCitation +from .answersearchresult import AnswerSearchResult +from typing import List, Optional +from youdotcom.types import BaseModel + + +class AnswerResults(BaseModel): + r"""Search results grouped by result type.""" + + web: Optional[List[AnswerSearchResult]] = None + r"""All web search results considered during answer synthesis.""" + + +class AnswerResponse(BaseModel): + r"""A synthesized answer with citations and supporting search results.""" + + answer: str + r"""The synthesized response with numbered inline citations that reference items in the ``citations`` array.""" + + citations: Optional[List[AnswerCitation]] = None + r"""The sources cited in the answer, in citation order.""" + + results: Optional[AnswerResults] = None + r"""Search results grouped by result type.""" diff --git a/src/youdotcom/models/answersearchresult.py b/src/youdotcom/models/answersearchresult.py new file mode 100644 index 0000000..3286c63 --- /dev/null +++ b/src/youdotcom/models/answersearchresult.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from typing import List, Optional +from youdotcom.types import BaseModel + + +class AnswerSearchResult(BaseModel): + r"""A web search result used during answer synthesis.""" + + url: str + r"""The URL of the source webpage.""" + + title: str + r"""The title of the source webpage.""" + + snippets: Optional[List[str]] = None + r"""Text snippets from the search result that preview its content.""" + + page_age: Optional[str] = None + r"""The publication date or age supplied by the search result.""" diff --git a/src/youdotcom/models/computetool.py b/src/youdotcom/models/computetool.py deleted file mode 100644 index 1253881..0000000 --- a/src/youdotcom/models/computetool.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ComputeToolTypedDict(TypedDict): - type: Literal["compute"] - r"""Setting this value to \"compute\" is mandatory to use the compute agent.""" - - -class ComputeTool(BaseModel): - TYPE: Annotated[ - Annotated[Literal["compute"], AfterValidator(validate_const("compute"))], - pydantic.Field(alias="type"), - ] = "compute" - r"""Setting this value to \"compute\" is mandatory to use the compute agent.""" - - -try: - ComputeTool.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/contents.py b/src/youdotcom/models/contents.py index 37d69b8..f2423a3 100644 --- a/src/youdotcom/models/contents.py +++ b/src/youdotcom/models/contents.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from pydantic import model_serializer diff --git a/src/youdotcom/models/contentsformats.py b/src/youdotcom/models/contentsformats.py index 91f16e0..c7d8ee9 100644 --- a/src/youdotcom/models/contentsformats.py +++ b/src/youdotcom/models/contentsformats.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/contentsmetadata.py b/src/youdotcom/models/contentsmetadata.py index b6b8e50..bba55c3 100644 --- a/src/youdotcom/models/contentsmetadata.py +++ b/src/youdotcom/models/contentsmetadata.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from pydantic import model_serializer diff --git a/src/youdotcom/models/contentsop.py b/src/youdotcom/models/contentsop.py index 9e10292..974ff45 100644 --- a/src/youdotcom/models/contentsop.py +++ b/src/youdotcom/models/contentsop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .contentsformats import ContentsFormats diff --git a/src/youdotcom/models/country.py b/src/youdotcom/models/country.py index 720e606..8169402 100644 --- a/src/youdotcom/models/country.py +++ b/src/youdotcom/models/country.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/customagentrunsrequest.py b/src/youdotcom/models/customagentrunsrequest.py deleted file mode 100644 index 181e560..0000000 --- a/src/youdotcom/models/customagentrunsrequest.py +++ /dev/null @@ -1,43 +0,0 @@ -"""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 CustomAgentRunsRequestTypedDict(TypedDict): - agent: str - r"""Set the value to a Custom Agent's ID. Learn how to obtain an agent ID here [Create Custom Agents](https://docs.you.com/agents/custom/create-agents).""" - input: str - r"""The question you'd like to ask the agent""" - stream: NotRequired[bool] - r"""Must be set to `true` when you want to stream the agent response as it's being generated, and `false` when you want the response to return after the agent has finished.""" - - -class CustomAgentRunsRequest(BaseModel): - agent: str - r"""Set the value to a Custom Agent's ID. Learn how to obtain an agent ID here [Create Custom Agents](https://docs.you.com/agents/custom/create-agents).""" - - input: str - r"""The question you'd like to ask the agent""" - - stream: Optional[bool] = False - r"""Must be set to `true` when you want to stream the agent response as it's being generated, and `false` when you want the response to return after the agent has finished.""" - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["stream"]) - 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/expressagentrunsrequest.py b/src/youdotcom/models/expressagentrunsrequest.py deleted file mode 100644 index 8211741..0000000 --- a/src/youdotcom/models/expressagentrunsrequest.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from .websearchtool import WebSearchTool, WebSearchToolTypedDict -import pydantic -from pydantic import model_serializer -from pydantic.functional_validators import AfterValidator -from typing import List, Literal, Optional -from typing_extensions import Annotated, NotRequired, TypedDict -from youdotcom.types import BaseModel, UNSET_SENTINEL -from youdotcom.utils import validate_const - - -class ExpressAgentRunsRequestTypedDict(TypedDict): - input: str - r"""The question you'd like to ask the agent""" - agent: Literal["express"] - r"""Setting this value to \"express\" is mandatory to use the express agent.""" - stream: NotRequired[bool] - r"""Must be set to `true` when you want to stream the express agent response as it's being generated, and `false` when you want the response to return after the agent has finished.""" - tools: NotRequired[List[WebSearchToolTypedDict]] - r"""You can optionally ground the express agent response using results fetched from the web (max 1 web search)""" - - -class ExpressAgentRunsRequest(BaseModel): - input: str - r"""The question you'd like to ask the agent""" - - AGENT: Annotated[ - Annotated[Literal["express"], AfterValidator(validate_const("express"))], - pydantic.Field(alias="agent"), - ] = "express" - r"""Setting this value to \"express\" is mandatory to use the express agent.""" - - stream: Optional[bool] = False - r"""Must be set to `true` when you want to stream the express agent response as it's being generated, and `false` when you want the response to return after the agent has finished.""" - - tools: Optional[List[WebSearchTool]] = None - r"""You can optionally ground the express agent response using results fetched from the web (max 1 web search)""" - - @model_serializer(mode="wrap") - def serialize_model(self, handler): - optional_fields = set(["stream", "tools"]) - 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 - - -try: - ExpressAgentRunsRequest.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/finance_researchop.py b/src/youdotcom/models/finance_researchop.py index 47259fd..f28fc6d 100644 --- a/src/youdotcom/models/finance_researchop.py +++ b/src/youdotcom/models/finance_researchop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .financeresearcheffort import FinanceResearchEffort diff --git a/src/youdotcom/models/financeresearcheffort.py b/src/youdotcom/models/financeresearcheffort.py index 909b887..cc72caf 100644 --- a/src/youdotcom/models/financeresearcheffort.py +++ b/src/youdotcom/models/financeresearcheffort.py @@ -1,7 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" -# Manual overlay: `lite` tier added via overlays/python_overlay.yaml -# until the upstream OpenAPI spec includes it. See that file for details. from __future__ import annotations from enum import Enum @@ -11,11 +8,9 @@ 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: - - `lite`: Returns answers quickly. Good for straightforward financial questions that just need a fast, reliable answer. - `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. """ - LITE = "lite" DEEP = "deep" EXHAUSTIVE = "exhaustive" diff --git a/src/youdotcom/models/freshness.py b/src/youdotcom/models/freshness.py index 83281eb..5228076 100644 --- a/src/youdotcom/models/freshness.py +++ b/src/youdotcom/models/freshness.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/freshnessvalue.py b/src/youdotcom/models/freshnessvalue.py index 66ff778..8742860 100644 --- a/src/youdotcom/models/freshnessvalue.py +++ b/src/youdotcom/models/freshnessvalue.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .freshness import Freshness diff --git a/src/youdotcom/models/getresearchtaskop.py b/src/youdotcom/models/getresearchtaskop.py index 0330943..914f673 100644 --- a/src/youdotcom/models/getresearchtaskop.py +++ b/src/youdotcom/models/getresearchtaskop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from typing_extensions import Annotated, TypedDict diff --git a/src/youdotcom/models/language.py b/src/youdotcom/models/language.py index 83704f1..852c77d 100644 --- a/src/youdotcom/models/language.py +++ b/src/youdotcom/models/language.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/livecrawl.py b/src/youdotcom/models/livecrawl.py index c28464e..aac7e1d 100644 --- a/src/youdotcom/models/livecrawl.py +++ b/src/youdotcom/models/livecrawl.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/livecrawlformats.py b/src/youdotcom/models/livecrawlformats.py index ceca02d..18eb4cd 100644 --- a/src/youdotcom/models/livecrawlformats.py +++ b/src/youdotcom/models/livecrawlformats.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/newsresult.py b/src/youdotcom/models/newsresult.py index 9a89295..d414648 100644 --- a/src/youdotcom/models/newsresult.py +++ b/src/youdotcom/models/newsresult.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .contents import Contents, ContentsTypedDict diff --git a/src/youdotcom/models/reportverbosity.py b/src/youdotcom/models/reportverbosity.py deleted file mode 100644 index 14a8b43..0000000 --- a/src/youdotcom/models/reportverbosity.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from enum import Enum - - -class ReportVerbosity(str, Enum): - r"""Select whether to receive a medium or high length model response.""" - - MEDIUM = "medium" - HIGH = "high" diff --git a/src/youdotcom/models/researcheffort.py b/src/youdotcom/models/researcheffort.py index 506ea33..869f169 100644 --- a/src/youdotcom/models/researcheffort.py +++ b/src/youdotcom/models/researcheffort.py @@ -1,7 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" -# Manual overlay: `frontier` tier added via overlays/python_overlay.yaml -# until the upstream OpenAPI spec includes it. See that file for details. from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/researchop.py b/src/youdotcom/models/researchop.py index 15349a0..151bc33 100644 --- a/src/youdotcom/models/researchop.py +++ b/src/youdotcom/models/researchop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .researcheffort import ResearchEffort diff --git a/src/youdotcom/models/researchresponse.py b/src/youdotcom/models/researchresponse.py index 8e0ef6c..f1a29b1 100644 --- a/src/youdotcom/models/researchresponse.py +++ b/src/youdotcom/models/researchresponse.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/researchtaskstreamevent.py b/src/youdotcom/models/researchtaskstreamevent.py index fe7f123..6596a05 100644 --- a/src/youdotcom/models/researchtaskstreamevent.py +++ b/src/youdotcom/models/researchtaskstreamevent.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/researchtool.py b/src/youdotcom/models/researchtool.py deleted file mode 100644 index 445f60c..0000000 --- a/src/youdotcom/models/researchtool.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from .reportverbosity import ReportVerbosity -from .searcheffort import SearchEffort -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ResearchToolTypedDict(TypedDict): - search_effort: SearchEffort - r"""This parameter maps to different configurations regarding the depth of research the tool can perform. Its values range from `low`, `medium` to `high`. - - Alternatively, use `auto` mode for a more dynamic search approach, allowing the tool the freedom to adjust its subparameters. - """ - report_verbosity: ReportVerbosity - r"""Select whether to receive a medium or high length model response.""" - type: Literal["research"] - r"""Setting this value to \"research\" is mandatory to use the research agent.""" - - -class ResearchTool(BaseModel): - search_effort: SearchEffort - r"""This parameter maps to different configurations regarding the depth of research the tool can perform. Its values range from `low`, `medium` to `high`. - - Alternatively, use `auto` mode for a more dynamic search approach, allowing the tool the freedom to adjust its subparameters. - """ - - report_verbosity: ReportVerbosity - r"""Select whether to receive a medium or high length model response.""" - - TYPE: Annotated[ - Annotated[Literal["research"], AfterValidator(validate_const("research"))], - pydantic.Field(alias="type"), - ] = "research" - r"""Setting this value to \"research\" is mandatory to use the research agent.""" - - -try: - ResearchTool.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/response_created.py b/src/youdotcom/models/response_created.py deleted file mode 100644 index 509085d..0000000 --- a/src/youdotcom/models/response_created.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ResponseCreatedTypedDict(TypedDict): - r"""SSE event signifying the response stream has been created""" - - seq_id: int - type: Literal["response.created"] - - -class ResponseCreated(BaseModel): - r"""SSE event signifying the response stream has been created""" - - seq_id: int - - TYPE: Annotated[ - Annotated[ - Literal["response.created"], - AfterValidator(validate_const("response.created")), - ], - pydantic.Field(alias="type"), - ] = "response.created" - - -try: - ResponseCreated.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/response_done.py b/src/youdotcom/models/response_done.py deleted file mode 100644 index 599f8a7..0000000 --- a/src/youdotcom/models/response_done.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ResponseDoneResponseTypedDict(TypedDict): - run_time_ms: str - r"""Total runtime in milliseconds""" - finished: bool - r"""Whether the response is complete""" - - -class ResponseDoneResponse(BaseModel): - run_time_ms: str - r"""Total runtime in milliseconds""" - - finished: bool - r"""Whether the response is complete""" - - -class ResponseDoneTypedDict(TypedDict): - seq_id: int - response: ResponseDoneResponseTypedDict - type: Literal["response.done"] - - -class ResponseDone(BaseModel): - seq_id: int - - response: ResponseDoneResponse - - TYPE: Annotated[ - Annotated[ - Literal["response.done"], AfterValidator(validate_const("response.done")) - ], - pydantic.Field(alias="type"), - ] = "response.done" - - -try: - ResponseDone.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/response_output_content_full.py b/src/youdotcom/models/response_output_content_full.py deleted file mode 100644 index b2bd70c..0000000 --- a/src/youdotcom/models/response_output_content_full.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from .agentrunsresponsewebsearchresult import ( - AgentRunsResponseWebSearchResult, - AgentRunsResponseWebSearchResultTypedDict, -) -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import List, Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ResponseOutputContentFullResponseTypedDict(TypedDict): - output_index: int - full: List[AgentRunsResponseWebSearchResultTypedDict] - r"""Complete web search results""" - type: Literal["web_search.results"] - - -class ResponseOutputContentFullResponse(BaseModel): - output_index: int - - full: List[AgentRunsResponseWebSearchResult] - r"""Complete web search results""" - - TYPE: Annotated[ - Annotated[ - Literal["web_search.results"], - AfterValidator(validate_const("web_search.results")), - ], - pydantic.Field(alias="type"), - ] = "web_search.results" - - -class ResponseOutputContentFullTypedDict(TypedDict): - seq_id: int - response: ResponseOutputContentFullResponseTypedDict - type: Literal["response.output_content.full"] - - -class ResponseOutputContentFull(BaseModel): - seq_id: int - - response: ResponseOutputContentFullResponse - - TYPE: Annotated[ - Annotated[ - Literal["response.output_content.full"], - AfterValidator(validate_const("response.output_content.full")), - ], - pydantic.Field(alias="type"), - ] = "response.output_content.full" - - -try: - ResponseOutputContentFullResponse.model_rebuild() -except NameError: - pass -try: - ResponseOutputContentFull.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/response_output_item_added.py b/src/youdotcom/models/response_output_item_added.py deleted file mode 100644 index 7294962..0000000 --- a/src/youdotcom/models/response_output_item_added.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ResponseOutputItemAddedResponseTypedDict(TypedDict): - output_index: int - r"""The index of the output item in the response""" - - -class ResponseOutputItemAddedResponse(BaseModel): - output_index: int - r"""The index of the output item in the response""" - - -class ResponseOutputItemAddedTypedDict(TypedDict): - r"""SSE event signifying an output item has been added""" - - seq_id: int - response: ResponseOutputItemAddedResponseTypedDict - type: Literal["response.output_item.added"] - - -class ResponseOutputItemAdded(BaseModel): - r"""SSE event signifying an output item has been added""" - - seq_id: int - - response: ResponseOutputItemAddedResponse - - TYPE: Annotated[ - Annotated[ - Literal["response.output_item.added"], - AfterValidator(validate_const("response.output_item.added")), - ], - pydantic.Field(alias="type"), - ] = "response.output_item.added" - - -try: - ResponseOutputItemAdded.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/response_output_item_done.py b/src/youdotcom/models/response_output_item_done.py deleted file mode 100644 index 54f4cbc..0000000 --- a/src/youdotcom/models/response_output_item_done.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ResponseOutputItemDoneResponseTypedDict(TypedDict): - output_index: int - - -class ResponseOutputItemDoneResponse(BaseModel): - output_index: int - - -class ResponseOutputItemDoneTypedDict(TypedDict): - seq_id: int - response: ResponseOutputItemDoneResponseTypedDict - type: Literal["response.output_item.done"] - - -class ResponseOutputItemDone(BaseModel): - seq_id: int - - response: ResponseOutputItemDoneResponse - - TYPE: Annotated[ - Annotated[ - Literal["response.output_item.done"], - AfterValidator(validate_const("response.output_item.done")), - ], - pydantic.Field(alias="type"), - ] = "response.output_item.done" - - -try: - ResponseOutputItemDone.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/response_output_text_delta.py b/src/youdotcom/models/response_output_text_delta.py deleted file mode 100644 index 36aa5a4..0000000 --- a/src/youdotcom/models/response_output_text_delta.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ResponseOutputTextDeltaResponseTypedDict(TypedDict): - output_index: int - delta: str - r"""Incremental text content""" - type: Literal["message.answer"] - - -class ResponseOutputTextDeltaResponse(BaseModel): - output_index: int - - delta: str - r"""Incremental text content""" - - TYPE: Annotated[ - Annotated[ - Literal["message.answer"], AfterValidator(validate_const("message.answer")) - ], - pydantic.Field(alias="type"), - ] = "message.answer" - - -class ResponseOutputTextDeltaTypedDict(TypedDict): - seq_id: int - response: ResponseOutputTextDeltaResponseTypedDict - type: Literal["response.output_text.delta"] - - -class ResponseOutputTextDelta(BaseModel): - seq_id: int - - response: ResponseOutputTextDeltaResponse - - TYPE: Annotated[ - Annotated[ - Literal["response.output_text.delta"], - AfterValidator(validate_const("response.output_text.delta")), - ], - pydantic.Field(alias="type"), - ] = "response.output_text.delta" - - -try: - ResponseOutputTextDeltaResponse.model_rebuild() -except NameError: - pass -try: - ResponseOutputTextDelta.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/response_starting.py b/src/youdotcom/models/response_starting.py deleted file mode 100644 index 438a191..0000000 --- a/src/youdotcom/models/response_starting.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class ResponseStartingTypedDict(TypedDict): - r"""SSE event signifying the response is starting""" - - seq_id: int - type: Literal["response.starting"] - - -class ResponseStarting(BaseModel): - r"""SSE event signifying the response is starting""" - - seq_id: int - - TYPE: Annotated[ - Annotated[ - Literal["response.starting"], - AfterValidator(validate_const("response.starting")), - ], - pydantic.Field(alias="type"), - ] = "response.starting" - - -try: - ResponseStarting.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/models/safesearch.py b/src/youdotcom/models/safesearch.py index 5a64287..bb32a49 100644 --- a/src/youdotcom/models/safesearch.py +++ b/src/youdotcom/models/safesearch.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from enum import Enum diff --git a/src/youdotcom/models/searcheffort.py b/src/youdotcom/models/searcheffort.py deleted file mode 100644 index cec092e..0000000 --- a/src/youdotcom/models/searcheffort.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from enum import Enum - - -class SearchEffort(str, Enum): - r"""This parameter maps to different configurations regarding the depth of research the tool can perform. Its values range from `low`, `medium` to `high`. - - Alternatively, use `auto` mode for a more dynamic search approach, allowing the tool the freedom to adjust its subparameters. - """ - - AUTO = "auto" - LOW = "low" - MEDIUM = "medium" - HIGH = "high" diff --git a/src/youdotcom/models/searchmetadata.py b/src/youdotcom/models/searchmetadata.py index 758b0c2..f788e74 100644 --- a/src/youdotcom/models/searchmetadata.py +++ b/src/youdotcom/models/searchmetadata.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from pydantic import model_serializer diff --git a/src/youdotcom/models/searchop.py b/src/youdotcom/models/searchop.py index 358a4fa..99c1156 100644 --- a/src/youdotcom/models/searchop.py +++ b/src/youdotcom/models/searchop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .country import Country diff --git a/src/youdotcom/models/searchpostop.py b/src/youdotcom/models/searchpostop.py deleted file mode 100644 index e0f8b5b..0000000 --- a/src/youdotcom/models/searchpostop.py +++ /dev/null @@ -1,8 +0,0 @@ -"""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 index df94c4c..247db23 100644 --- a/src/youdotcom/models/searchrequestbody.py +++ b/src/youdotcom/models/searchrequestbody.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .country import Country diff --git a/src/youdotcom/models/searchresponse.py b/src/youdotcom/models/searchresponse.py index ee9313e..e17262f 100644 --- a/src/youdotcom/models/searchresponse.py +++ b/src/youdotcom/models/searchresponse.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .newsresult import NewsResult, NewsResultTypedDict diff --git a/src/youdotcom/models/security.py b/src/youdotcom/models/security.py index 2313b52..765679f 100644 --- a/src/youdotcom/models/security.py +++ b/src/youdotcom/models/security.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from pydantic import model_serializer diff --git a/src/youdotcom/models/streamresearchtaskop.py b/src/youdotcom/models/streamresearchtaskop.py index 6b57b1d..8bc0384 100644 --- a/src/youdotcom/models/streamresearchtaskop.py +++ b/src/youdotcom/models/streamresearchtaskop.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from pydantic import model_serializer diff --git a/src/youdotcom/models/taskdetail.py b/src/youdotcom/models/taskdetail.py index f039294..a8e4a6c 100644 --- a/src/youdotcom/models/taskdetail.py +++ b/src/youdotcom/models/taskdetail.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from datetime import datetime diff --git a/src/youdotcom/models/taskresponse.py b/src/youdotcom/models/taskresponse.py index 490476d..3f5fe9a 100644 --- a/src/youdotcom/models/taskresponse.py +++ b/src/youdotcom/models/taskresponse.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from datetime import datetime diff --git a/src/youdotcom/models/verbosity.py b/src/youdotcom/models/verbosity.py deleted file mode 100644 index 666d75a..0000000 --- a/src/youdotcom/models/verbosity.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -from enum import Enum - - -class Verbosity(str, Enum): - r"""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.""" - - MEDIUM = "medium" - HIGH = "high" diff --git a/src/youdotcom/models/webresult.py b/src/youdotcom/models/webresult.py index 27df725..73edde0 100644 --- a/src/youdotcom/models/webresult.py +++ b/src/youdotcom/models/webresult.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations from .contents import Contents, ContentsTypedDict diff --git a/src/youdotcom/models/websearchtool.py b/src/youdotcom/models/websearchtool.py deleted file mode 100644 index 7834ec3..0000000 --- a/src/youdotcom/models/websearchtool.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from __future__ import annotations -import pydantic -from pydantic.functional_validators import AfterValidator -from typing import Literal -from typing_extensions import Annotated, TypedDict -from youdotcom.types import BaseModel -from youdotcom.utils import validate_const - - -class WebSearchToolTypedDict(TypedDict): - type: Literal["web_search"] - r"""Setting this value to \"web_search\" is mandatory to use the web_search tool.""" - - -class WebSearchTool(BaseModel): - TYPE: Annotated[ - Annotated[Literal["web_search"], AfterValidator(validate_const("web_search"))], - pydantic.Field(alias="type"), - ] = "web_search" - r"""Setting this value to \"web_search\" is mandatory to use the web_search tool.""" - - -try: - WebSearchTool.model_rebuild() -except NameError: - pass diff --git a/src/youdotcom/research_helpers.py b/src/youdotcom/research_helpers.py index a4b0078..e56bfbf 100644 --- a/src/youdotcom/research_helpers.py +++ b/src/youdotcom/research_helpers.py @@ -1,7 +1,6 @@ """Hand-maintained research workflow helpers. -This module is NOT regenerated by Speakeasy. It adds convenience helpers on -top of the auto-generated research endpoints: +Convenience helpers on top of the research endpoints: - ``research_background`` / ``research_background_async``: Submit a research task with ``background=True`` and return the ``TaskResponse`` directly so diff --git a/src/youdotcom/runs.py b/src/youdotcom/runs.py deleted file mode 100644 index 0c63569..0000000 --- a/src/youdotcom/runs.py +++ /dev/null @@ -1,293 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from .basesdk import BaseSDK -from typing import Any, Mapping, Optional, Union, cast -from youdotcom import errors, models, utils -from youdotcom._hooks import HookContext -from youdotcom.types import BaseModel, OptionalNullable, UNSET -from youdotcom.utils import eventstreaming, get_security_from_env -from youdotcom.utils.unmarshal_json_response import unmarshal_json_response - - -class Runs(BaseSDK): - def create( - self, - *, - request: Union[models.AgentsRunsRequest, models.AgentsRunsRequestTypedDict], - retries: OptionalNullable[utils.RetryConfig] = UNSET, - server_url: Optional[str] = None, - timeout_ms: Optional[int] = None, - http_headers: Optional[Mapping[str, str]] = None, - ) -> 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: - - - **Express Agent**: Fast responses with optional web search (max 1 search) - - **Advanced Agent**: Complex queries with multi-turn reasoning, planning, and tool usage - - **Custom Agent**: User-configured assistants created in the You.com UI - - The response format depends on the `stream` parameter - either a complete JSON payload or Server-Sent Events (SSE). - - - :param request: The request object to send. - :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.AGENTS_RUNS_OP_SERVERS[0] - - if not isinstance(request, BaseModel): - request = utils.unmarshal(request, models.AgentsRunsRequest) - request = cast(models.AgentsRunsRequest, request) - - req = self._build_request( - method="POST", - path="/v1/agents/runs", - 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="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( - request, False, False, "json", models.AgentsRunsRequest - ), - 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="AgentsRuns", - oauth2_scopes=None, - security_source=get_security_from_env( - self.sdk_configuration.security, models.Security - ), - tags=["agents.runs"], - extensions=None, - ), - request=req, - is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), - stream=getattr(request, "stream", False) is True, - retry_config=retry_config, - ) - - response_data: Any = None - if utils.match_response(http_res, "200", "application/json"): - http_res_text = utils.stream_to_text(http_res) - return unmarshal_json_response( - models.AgentRunsBatchResponse, http_res, http_res_text - ) - if utils.match_response(http_res, "200", "text/event-stream"): - return eventstreaming.EventStream( - http_res, - lambda raw: unmarshal_json_response( - models.AgentRunsStreamingResponse, http_res, raw - ), - client_ref=self, - ) - if utils.match_response(http_res, "400", "application/json"): - http_res_text = utils.stream_to_text(http_res) - response_data = unmarshal_json_response( - errors.AgentRuns400ResponseErrorData, http_res, http_res_text - ) - raise errors.AgentRuns400ResponseError( - response_data, http_res, http_res_text - ) - if utils.match_response(http_res, "401", "application/json"): - http_res_text = utils.stream_to_text(http_res) - response_data = unmarshal_json_response( - errors.AgentRuns401ResponseErrorData, http_res, http_res_text - ) - raise errors.AgentRuns401ResponseError( - response_data, http_res, http_res_text - ) - if utils.match_response(http_res, "422", "application/json"): - http_res_text = utils.stream_to_text(http_res) - response_data = unmarshal_json_response( - errors.AgentRuns422ResponseErrorData, http_res, http_res_text - ) - raise errors.AgentRuns422ResponseError( - 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 create_async( - self, - *, - request: Union[models.AgentsRunsRequest, models.AgentsRunsRequestTypedDict], - retries: OptionalNullable[utils.RetryConfig] = UNSET, - server_url: Optional[str] = None, - timeout_ms: Optional[int] = None, - http_headers: Optional[Mapping[str, str]] = None, - ) -> 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: - - - **Express Agent**: Fast responses with optional web search (max 1 search) - - **Advanced Agent**: Complex queries with multi-turn reasoning, planning, and tool usage - - **Custom Agent**: User-configured assistants created in the You.com UI - - The response format depends on the `stream` parameter - either a complete JSON payload or Server-Sent Events (SSE). - - - :param request: The request object to send. - :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.AGENTS_RUNS_OP_SERVERS[0] - - if not isinstance(request, BaseModel): - request = utils.unmarshal(request, models.AgentsRunsRequest) - request = cast(models.AgentsRunsRequest, request) - - req = self._build_request_async( - method="POST", - path="/v1/agents/runs", - 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="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( - request, False, False, "json", models.AgentsRunsRequest - ), - 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="AgentsRuns", - oauth2_scopes=None, - security_source=get_security_from_env( - self.sdk_configuration.security, models.Security - ), - tags=["agents.runs"], - extensions=None, - ), - request=req, - is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), - stream=getattr(request, "stream", False) is True, - retry_config=retry_config, - ) - - response_data: Any = None - if utils.match_response(http_res, "200", "application/json"): - http_res_text = await utils.stream_to_text_async(http_res) - return unmarshal_json_response( - models.AgentRunsBatchResponse, http_res, http_res_text - ) - if utils.match_response(http_res, "200", "text/event-stream"): - return eventstreaming.EventStreamAsync( - http_res, - lambda raw: unmarshal_json_response( - models.AgentRunsStreamingResponse, http_res, raw - ), - client_ref=self, - ) - if utils.match_response(http_res, "400", "application/json"): - http_res_text = await utils.stream_to_text_async(http_res) - response_data = unmarshal_json_response( - errors.AgentRuns400ResponseErrorData, http_res, http_res_text - ) - raise errors.AgentRuns400ResponseError( - response_data, http_res, http_res_text - ) - 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.AgentRuns401ResponseErrorData, http_res, http_res_text - ) - raise errors.AgentRuns401ResponseError( - response_data, http_res, http_res_text - ) - if utils.match_response(http_res, "422", "application/json"): - http_res_text = await utils.stream_to_text_async(http_res) - response_data = unmarshal_json_response( - errors.AgentRuns422ResponseErrorData, http_res, http_res_text - ) - raise errors.AgentRuns422ResponseError( - 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 - ) diff --git a/src/youdotcom/sdk.py b/src/youdotcom/sdk.py index 166fce4..d9f010c 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from .basesdk import BaseSDK from .httpclient import AsyncHttpClient, ClientOwner, HttpClient, close_clients @@ -6,8 +6,6 @@ from .utils.logger import Logger, get_default_logger from .utils.retries import RetryConfig import httpx -import importlib -import sys from typing import ( Any, Callable, @@ -16,46 +14,32 @@ List, Mapping, Optional, - TYPE_CHECKING, Union, cast, ) import weakref from youdotcom import errors, models, utils from youdotcom._hooks import HookContext, SDKHooks +from youdotcom._shims import ContentsShim, SearchShim from youdotcom.types import OptionalNullable, UNSET from youdotcom.utils import eventstreaming, get_security_from_env from youdotcom.utils.unmarshal_json_response import unmarshal_json_response -if TYPE_CHECKING: - from youdotcom.agents import Agents - from youdotcom.contents_sdk import ContentsSDK - from youdotcom.search import Search - class You(BaseSDK): - r"""You.com API: Unified API for Express, Advanced, and Custom Agents from You.com + r"""You.com API: Unified API for search, answers, research, and content 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 Finance-focused multi-step research with competitive accuracy at same price points and latencies as the Research API Comprehensive API for You.com services: - - **Agents API**: Execute queries using Express, Advanced, and Custom AI agents + - **Answer API**: Get synthesized, citation-backed answers grounded in real-time web results - **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 """ - agents: "Agents" - search: "Search" - contents: "ContentsSDK" - _sub_sdk_map = { - "agents": ("youdotcom.agents", "Agents"), - "search": ("youdotcom.search", "Search"), - "contents": ("youdotcom.contents_sdk", "ContentsSDK"), - } - def __init__( self, api_key_auth: Optional[ @@ -103,13 +87,17 @@ def __init__( ), "The provided async_client must implement the AsyncHttpClient protocol." security: Any = None - if api_key_auth is None: - security = None - elif callable(api_key_auth): + # Normalize empty strings to None so an empty X-API-Key header + # isn't sent (e.g. You(api_key_auth=os.getenv("YDC_API_KEY", ""))) + if callable(api_key_auth): # pylint: disable=unnecessary-lambda-assignment - security = lambda: models.Security(api_key_auth=api_key_auth()) - else: + security = lambda: models.Security( + api_key_auth=(api_key_auth() or None) + ) + elif api_key_auth: security = models.Security(api_key_auth=api_key_auth) + else: + security = None if server_url is not None: if url_params is not None: @@ -149,43 +137,12 @@ def __init__( self.sdk_configuration.async_client_supplied, ) - def dynamic_import(self, modname, retries=3): - for attempt in range(retries): - try: - return importlib.import_module(modname) - except KeyError: - # Clear any half-initialized module and retry - sys.modules.pop(modname, None) - if attempt == retries - 1: - break - raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") - - def __getattr__(self, name: str): - if name in self._sub_sdk_map: - module_path, class_name = self._sub_sdk_map[name] - try: - module = self.dynamic_import(module_path) - klass = getattr(module, class_name) - instance = klass(self.sdk_configuration, parent_ref=self) - setattr(self, name, instance) - return instance - except ImportError as e: - raise AttributeError( - f"Failed to import module {module_path} for attribute {name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to find class {class_name} in module {module_path} for attribute {name}: {e}" - ) from e - - raise AttributeError( - f"'{type(self).__name__}' object has no attribute '{name}'" - ) - - def __dir__(self): - default_attrs = list(super().__dir__()) - lazy_attrs = list(self._sub_sdk_map.keys()) - return sorted(list(set(default_attrs + lazy_attrs))) + # Backward-compat shims: you.search.unified(), you.contents.generate() + # still work but emit DeprecationWarning. + # you.search(query=...), you.contents(urls=...) (the new API) go + # through __call__ with no warning. + self.search = SearchShim(self) + self.contents = ContentsShim(self) def __enter__(self): return self @@ -209,54 +166,579 @@ 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( + async def search_async(self, **kwargs: Any) -> models.SearchResponse: + """Async variant of :meth:`search`.""" + return await self._search_async_impl(**kwargs) + + async def contents_async(self, **kwargs: Any) -> List[models.ContentsResponse]: + """Async variant of :meth:`contents`.""" + return await self._contents_async_impl(**kwargs) + + def answer( 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, + country: Optional[Union[str, models.Country]] = None, + language: Optional[Union[str, models.Language]] = None, include_domains: Optional[Iterable[str]] = None, exclude_domains: Optional[Iterable[str]] = None, boost_domains: Optional[Iterable[str]] = None, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.AnswerResponse: + r"""Returns a synthesized answer with citations from web search results. + + Provide a ``query`` and optional freshness, locale, and domain controls. + The response includes a markdown answer with inline citations, a + citations array with source URLs and supporting excerpts, and the web + results used to generate the answer. + + :param query: The search query used to retrieve relevant web results. + Max 400 characters. Search operators (``site:``, ``OR``, etc.) are + not supported. + :param freshness: Specifies the freshness of the results. One of ``day``, + ``week``, ``month``, ``year``, or ``YYYY-MM-DDtoYYYY-MM-DD``. + :param country: A supported country code that determines the geographical + focus of the web results. + :param language: A supported BCP 47 language tag that determines the + language of the web results. + :param include_domains: Domains to exclusively include. Cannot combine + with ``exclude_domains`` or ``boost_domains``. Max 500. + :param exclude_domains: Domains to exclude. Cannot combine with + ``include_domains``. Can combine with ``boost_domains``. Max 500. + :param boost_domains: Domains to prefer in ranking. Cannot combine with + ``include_domains``. Can combine with ``exclude_domains``. Max 500. + :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(None, None) + + body: dict = dict( + query=query, + freshness=freshness, + country=country.upper() if isinstance(country, str) else country, + language=language.upper() if isinstance(language, str) else language, + 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]]), + ) + request = models.AnswerRequestBody(**body) + + req = self._build_request( + method="POST", + path="/v1/answer", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=True, + request_has_path_params=False, + request_has_query_params=False, + 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.AnswerRequestBody + ), + 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="answer", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=["answer"], + 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.AnswerResponse, 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, "402", "application/json"): + response_data = unmarshal_json_response( + errors.PaymentRequiredResponseErrorData, http_res + ) + raise errors.PaymentRequiredResponseError(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 answer_async( + self, + *, + query: str, + freshness: Optional[ + Union[models.FreshnessValue, models.FreshnessValueTypedDict] + ] = None, + country: Optional[Union[str, models.Country]] = None, + language: Optional[Union[str, models.Language]] = None, + include_domains: Optional[Iterable[str]] = None, + exclude_domains: Optional[Iterable[str]] = None, + boost_domains: Optional[Iterable[str]] = None, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> models.AnswerResponse: + r"""Returns a synthesized answer with citations from web search results. + + Provide a ``query`` and optional freshness, locale, and domain controls. + The response includes a markdown answer with inline citations, a + citations array with source URLs and supporting excerpts, and the web + results used to generate the answer. + + :param query: The search query used to retrieve relevant web results. + Max 400 characters. Search operators (``site:``, ``OR``, etc.) are + not supported. + :param freshness: Specifies the freshness of the results. One of ``day``, + ``week``, ``month``, ``year``, or ``YYYY-MM-DDtoYYYY-MM-DD``. + :param country: A supported country code that determines the geographical + focus of the web results. + :param language: A supported BCP 47 language tag that determines the + language of the web results. + :param include_domains: Domains to exclusively include. Cannot combine + with ``exclude_domains`` or ``boost_domains``. Max 500. + :param exclude_domains: Domains to exclude. Cannot combine with + ``include_domains``. Can combine with ``boost_domains``. Max 500. + :param boost_domains: Domains to prefer in ranking. Cannot combine with + ``include_domains``. Can combine with ``exclude_domains``. Max 500. + :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(None, None) + + body: dict = dict( + query=query, + freshness=freshness, + country=country.upper() if isinstance(country, str) else country, + language=language.upper() if isinstance(language, str) else language, + 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]]), + ) + request = models.AnswerRequestBody(**body) + + req = self._build_request_async( + method="POST", + path="/v1/answer", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=True, + request_has_path_params=False, + request_has_query_params=False, + 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.AnswerRequestBody + ), + 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="answer", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=["answer"], + 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.AnswerResponse, 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, "402", "application/json"): + response_data = unmarshal_json_response( + errors.PaymentRequiredResponseErrorData, http_res + ) + raise errors.PaymentRequiredResponseError(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 _contents_impl( + self, + *, + 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, http_headers: Optional[Mapping[str, str]] = None, - ) -> models.SearchResponse: - r"""Returns a list of unified search results from web and news sources + ) -> List[models.ContentsResponse]: + r"""Returns the content of the web pages + + Returns the HTML or Markdown of a target webpage. + + :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 + :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(models.CONTENTS_OP_SERVERS[0], None) + + request = models.ContentsRequest( + 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( + method="POST", + path="/v1/contents", + 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.ContentsRequest + ), + 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="contents", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=["contents"], + 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(List[models.ContentsResponse], http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.ContentsUnauthorizedErrorData, http_res + ) + raise errors.ContentsUnauthorizedError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.ContentsForbiddenErrorData, http_res + ) + raise errors.ContentsForbiddenError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.ContentsInternalServerErrorData, http_res + ) + raise errors.ContentsInternalServerError(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 _contents_async_impl( + self, + *, + 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, + http_headers: Optional[Mapping[str, str]] = None, + ) -> List[models.ContentsResponse]: + r"""Returns the content of the web pages - 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. + Returns the HTML or Markdown of a target webpage. - `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 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 + :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(models.CONTENTS_OP_SERVERS[0], None) - :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`. + request = models.ContentsRequest( + 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( + method="POST", + path="/v1/contents", + 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.ContentsRequest + ), + 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="contents", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, models.Security + ), + tags=["contents"], + extensions=None, + ), + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + retry_config=retry_config, + ) - 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. + response_data: Any = None + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(List[models.ContentsResponse], http_res) + if utils.match_response(http_res, "401", "application/json"): + response_data = unmarshal_json_response( + errors.ContentsUnauthorizedErrorData, http_res + ) + raise errors.ContentsUnauthorizedError(response_data, http_res) + if utils.match_response(http_res, "403", "application/json"): + response_data = unmarshal_json_response( + errors.ContentsForbiddenErrorData, http_res + ) + raise errors.ContentsForbiddenError(response_data, http_res) + if utils.match_response(http_res, "500", "application/json"): + response_data = unmarshal_json_response( + errors.ContentsInternalServerErrorData, http_res + ) + raise errors.ContentsInternalServerError(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) - 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. + raise errors.YouDefaultError("Unexpected response received", http_res) - 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. + def _search_impl( + self, + *, + query: str, + count: Optional[int] = 10, + freshness: Optional[str] = None, + offset: Optional[int] = None, + country: Optional[str] = None, + language: Optional[str] = None, + safesearch: Optional[str] = None, + livecrawl: Optional[str] = None, + livecrawl_formats: Optional[Iterable[str]] = 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"""Search via POST /v1/search. + + Enum-typed parameters (``country``, ``safesearch``, ``livecrawl``, + ``freshness``) accept plain strings -- pydantic coerces them when + building the request body, so callers don't need to import enum classes. + + :param query: The search query used to retrieve relevant results from the web. + :param count: Max results per section (1-100). + :param freshness: ``"day"``, ``"week"``, ``"month"``, ``"year"``, or + ``"YYYY-MM-DDtoYYYY-MM-DD"``. + :param offset: Pagination offset (multiples of ``count``). + :param country: Country code for geographical focus. + :param language: BCP 47 language code (default ``"en"``). + :param safesearch: ``"strict"``, ``"moderate"``, or ``"off"``. + :param livecrawl: ``"web"``, ``"news"``, or ``"all"``. + :param livecrawl_formats: ``["html"]``, ``["markdown"]``, or both. + :param include_domains: Restrict results to these domains (<= 500). + :param exclude_domains: Exclude these domains (<= 500). + :param boost_domains: Boost these domains in ranking (<= 500). + :param crawl_timeout: Max seconds to wait for livecrawl (1-60, default 10). :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 @@ -270,15 +752,14 @@ def search_post( if server_url is not None: base_url = server_url else: - base_url = models.SEARCH_POST_OP_SERVERS[0] + base_url = self._get_url(models.SEARCH_OP_SERVERS[0], None) - request = models.SearchRequestBody( + body: dict[str, Any] = dict( query=query, count=count, freshness=freshness, offset=offset, - country=country, - language=language, + country=country.upper() if isinstance(country, str) else country, safesearch=safesearch, livecrawl=livecrawl, livecrawl_formats=utils.unmarshal( @@ -289,6 +770,9 @@ def search_post( boost_domains=utils.unmarshal(boost_domains, Optional[List[str]]), crawl_timeout=crawl_timeout, ) + if language is not None: + body["language"] = language.upper() if isinstance(language, str) else language + request = models.SearchRequestBody(**body) req = self._build_request( method="POST", @@ -322,12 +806,12 @@ def search_post( hook_ctx=HookContext( config=self.sdk_configuration, base_url=base_url or "", - operation_id="searchPost", + operation_id="search", oauth2_scopes=None, security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), - tags=None, + tags=["search"], extensions=None, ), request=req, @@ -367,20 +851,18 @@ def search_post( raise errors.YouDefaultError("Unexpected response received", http_res) - async def search_post_async( + async def _search_async_impl( self, *, query: str, count: Optional[int] = 10, - freshness: Optional[ - Union[models.FreshnessValue, models.FreshnessValueTypedDict] - ] = None, + freshness: Optional[str] = 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, + country: Optional[str] = None, + language: Optional[str] = None, + safesearch: Optional[str] = None, + livecrawl: Optional[str] = None, + livecrawl_formats: Optional[Iterable[str]] = None, include_domains: Optional[Iterable[str]] = None, exclude_domains: Optional[Iterable[str]] = None, boost_domains: Optional[Iterable[str]] = None, @@ -390,35 +872,9 @@ async def search_post_async( 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. + r"""Search via POST /v1/search. - 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. + Async variant of :meth:`search`. """ base_url = None url_variables = None @@ -428,15 +884,14 @@ async def search_post_async( if server_url is not None: base_url = server_url else: - base_url = models.SEARCH_POST_OP_SERVERS[0] + base_url = self._get_url(models.SEARCH_OP_SERVERS[0], None) - request = models.SearchRequestBody( + body: dict[str, Any] = dict( query=query, count=count, freshness=freshness, offset=offset, - country=country, - language=language, + country=country.upper() if isinstance(country, str) else country, safesearch=safesearch, livecrawl=livecrawl, livecrawl_formats=utils.unmarshal( @@ -447,6 +902,9 @@ async def search_post_async( boost_domains=utils.unmarshal(boost_domains, Optional[List[str]]), crawl_timeout=crawl_timeout, ) + if language is not None: + body["language"] = language.upper() if isinstance(language, str) else language + request = models.SearchRequestBody(**body) req = self._build_request_async( method="POST", @@ -480,12 +938,12 @@ async def search_post_async( hook_ctx=HookContext( config=self.sdk_configuration, base_url=base_url or "", - operation_id="searchPost", + operation_id="search", oauth2_scopes=None, security_source=get_security_from_env( self.sdk_configuration.security, models.Security ), - tags=None, + tags=["search"], extensions=None, ), request=req, diff --git a/src/youdotcom/sdkconfiguration.py b/src/youdotcom/sdkconfiguration.py index ea49a89..ec05e3e 100644 --- a/src/youdotcom/sdkconfiguration.py +++ b/src/youdotcom/sdkconfiguration.py @@ -1,7 +1,6 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from ._version import ( - __gen_version__, __openapi_doc_version__, __user_agent__, __version__, @@ -34,7 +33,6 @@ class SDKConfiguration: language: str = "python" openapi_doc_version: str = __openapi_doc_version__ sdk_version: str = __version__ - gen_version: str = __gen_version__ user_agent: str = __user_agent__ retry_config: OptionalNullable[RetryConfig] = Field(default_factory=lambda: UNSET) timeout_ms: Optional[int] = None diff --git a/src/youdotcom/search.py b/src/youdotcom/search.py deleted file mode 100644 index 9e69fa0..0000000 --- a/src/youdotcom/search.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -from .basesdk import BaseSDK -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 -from youdotcom.utils import get_security_from_env -from youdotcom.utils.unmarshal_json_response import unmarshal_json_response - - -class Search(BaseSDK): - def unified( - 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[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, - 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. - - `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: - :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: - :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 - :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_OP_SERVERS[0] - - request = models.SearchRequest( - 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=include_domains, - exclude_domains=exclude_domains, - boost_domains=boost_domains, - crawl_timeout=crawl_timeout, - ) - - req = self._build_request( - method="GET", - path="/v1/search", - base_url=base_url, - url_variables=url_variables, - request=request, - request_body_required=False, - 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, - 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="search", - oauth2_scopes=None, - security_source=get_security_from_env( - self.sdk_configuration.security, models.Security - ), - tags=["search"], - 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 unified_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[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, - 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. - - `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: - :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: - :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 - :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_OP_SERVERS[0] - - request = models.SearchRequest( - 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=include_domains, - exclude_domains=exclude_domains, - boost_domains=boost_domains, - crawl_timeout=crawl_timeout, - ) - - req = self._build_request_async( - method="GET", - path="/v1/search", - base_url=base_url, - url_variables=url_variables, - request=request, - request_body_required=False, - 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, - 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="search", - oauth2_scopes=None, - security_source=get_security_from_env( - self.sdk_configuration.security, models.Security - ), - tags=["search"], - 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) diff --git a/src/youdotcom/types/__init__.py b/src/youdotcom/types/__init__.py index faa2681..74b9dc1 100644 --- a/src/youdotcom/types/__init__.py +++ b/src/youdotcom/types/__init__.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from .base64fileinput import Base64EncodedString, Base64FileInput from .basemodel import ( diff --git a/src/youdotcom/types/base64fileinput.py b/src/youdotcom/types/base64fileinput.py index 862566f..9816c09 100644 --- a/src/youdotcom/types/base64fileinput.py +++ b/src/youdotcom/types/base64fileinput.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from __future__ import annotations diff --git a/src/youdotcom/types/basemodel.py b/src/youdotcom/types/basemodel.py index a9a640a..d0c2583 100644 --- a/src/youdotcom/types/basemodel.py +++ b/src/youdotcom/types/basemodel.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from pydantic import ConfigDict, model_serializer from pydantic import BaseModel as PydanticBaseModel diff --git a/src/youdotcom/utils/__init__.py b/src/youdotcom/utils/__init__.py index c48a36c..81eacf5 100644 --- a/src/youdotcom/utils/__init__.py +++ b/src/youdotcom/utils/__init__.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from typing import Any, TYPE_CHECKING, Callable, TypeVar import asyncio diff --git a/src/youdotcom/utils/annotations.py b/src/youdotcom/utils/annotations.py index 12e0aa4..188c8f6 100644 --- a/src/youdotcom/utils/annotations.py +++ b/src/youdotcom/utils/annotations.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from enum import Enum from typing import Any, Optional diff --git a/src/youdotcom/utils/datetimes.py b/src/youdotcom/utils/datetimes.py index adad247..6452522 100644 --- a/src/youdotcom/utils/datetimes.py +++ b/src/youdotcom/utils/datetimes.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from datetime import datetime, timedelta import sys diff --git a/src/youdotcom/utils/dynamic_imports.py b/src/youdotcom/utils/dynamic_imports.py index 673edf8..7f88737 100644 --- a/src/youdotcom/utils/dynamic_imports.py +++ b/src/youdotcom/utils/dynamic_imports.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from importlib import import_module import builtins diff --git a/src/youdotcom/utils/enums.py b/src/youdotcom/utils/enums.py index 3324e1b..b56be22 100644 --- a/src/youdotcom/utils/enums.py +++ b/src/youdotcom/utils/enums.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import enum import sys diff --git a/src/youdotcom/utils/eventstreaming.py b/src/youdotcom/utils/eventstreaming.py index a8d4fe5..09b85c1 100644 --- a/src/youdotcom/utils/eventstreaming.py +++ b/src/youdotcom/utils/eventstreaming.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import re import json diff --git a/src/youdotcom/utils/forms.py b/src/youdotcom/utils/forms.py index 193f264..5d1ccb0 100644 --- a/src/youdotcom/utils/forms.py +++ b/src/youdotcom/utils/forms.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import io from typing import ( diff --git a/src/youdotcom/utils/headers.py b/src/youdotcom/utils/headers.py index 37864cb..5e938f0 100644 --- a/src/youdotcom/utils/headers.py +++ b/src/youdotcom/utils/headers.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from typing import ( Any, diff --git a/src/youdotcom/utils/logger.py b/src/youdotcom/utils/logger.py index 6ae3abd..cd063d5 100644 --- a/src/youdotcom/utils/logger.py +++ b/src/youdotcom/utils/logger.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import httpx import logging diff --git a/src/youdotcom/utils/metadata.py b/src/youdotcom/utils/metadata.py index 5abddd5..a70634a 100644 --- a/src/youdotcom/utils/metadata.py +++ b/src/youdotcom/utils/metadata.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from typing import Optional, Type, TypeVar, Union from dataclasses import dataclass diff --git a/src/youdotcom/utils/queryparams.py b/src/youdotcom/utils/queryparams.py index c04e0db..f831b89 100644 --- a/src/youdotcom/utils/queryparams.py +++ b/src/youdotcom/utils/queryparams.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from typing import ( Any, diff --git a/src/youdotcom/utils/requestbodies.py b/src/youdotcom/utils/requestbodies.py index 591415a..17aba70 100644 --- a/src/youdotcom/utils/requestbodies.py +++ b/src/youdotcom/utils/requestbodies.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import io from dataclasses import dataclass diff --git a/src/youdotcom/utils/retries.py b/src/youdotcom/utils/retries.py index ca7b59e..2a7a52d 100644 --- a/src/youdotcom/utils/retries.py +++ b/src/youdotcom/utils/retries.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import asyncio import random diff --git a/src/youdotcom/utils/security.py b/src/youdotcom/utils/security.py index 2468bf1..d7fb202 100644 --- a/src/youdotcom/utils/security.py +++ b/src/youdotcom/utils/security.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + import base64 @@ -78,9 +78,7 @@ def get_security_from_env(security: Any, security_class: Any) -> Optional[BaseMo # Hand-applied env-var precedence: `YDC_API_KEY` is the canonical 2.4.0+ # env var; `YOU_API_KEY_AUTH` is the legacy 2.3.x name preserved as a - # fallback for users who haven't migrated yet. Speakeasy overrides this - # block on every regeneration, so the precedence must be re-applied - # after each `speakeasy run`. Covered by tests/test_security_env.py. + # fallback for users who haven't migrated yet. Covered by tests/test_security_env.py. if os.getenv("YDC_API_KEY"): security_dict["api_key_auth"] = os.getenv("YDC_API_KEY") elif os.getenv("YOU_API_KEY_AUTH"): diff --git a/src/youdotcom/utils/serializers.py b/src/youdotcom/utils/serializers.py index 1031ed9..7ca3db3 100644 --- a/src/youdotcom/utils/serializers.py +++ b/src/youdotcom/utils/serializers.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from decimal import Decimal import functools diff --git a/src/youdotcom/utils/unmarshal_json_response.py b/src/youdotcom/utils/unmarshal_json_response.py index 131b392..12d397d 100644 --- a/src/youdotcom/utils/unmarshal_json_response.py +++ b/src/youdotcom/utils/unmarshal_json_response.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from typing import Any, Optional, Type, TypeVar, overload diff --git a/src/youdotcom/utils/url.py b/src/youdotcom/utils/url.py index c78ccba..5c7be7f 100644 --- a/src/youdotcom/utils/url.py +++ b/src/youdotcom/utils/url.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from decimal import Decimal from typing import ( diff --git a/src/youdotcom/utils/values.py b/src/youdotcom/utils/values.py index dae01a4..6e25f4b 100644 --- a/src/youdotcom/utils/values.py +++ b/src/youdotcom/utils/values.py @@ -1,4 +1,4 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from datetime import datetime from enum import Enum diff --git a/tests/PERFORMANCE_TESTING.md b/tests/PERFORMANCE_TESTING.md index e91d623..01d9fac 100644 --- a/tests/PERFORMANCE_TESTING.md +++ b/tests/PERFORMANCE_TESTING.md @@ -7,8 +7,7 @@ This document describes how to use the comprehensive performance testing suite f The performance testing suite measures SDK latency vs API latency across all supported endpoint combinations: - **Search API**: 20+ test cases covering filters, livecrawl, pagination, etc. -- **Agents API**: 20+ test cases covering agent types, tool combinations, verbosity levels -- **Contents API**: 6 test cases covering formats and URL counts +- **Contents API**: 8 test cases covering formats and URL counts ## Quick Start @@ -52,12 +51,6 @@ pytest tests/test_performance.py -v pytest tests/test_performance.py::TestSearchPerformance -v ``` -### Agents Tests Only - -```bash -pytest tests/test_performance.py::TestAgentsPerformance -v -``` - ### Contents Tests Only ```bash @@ -90,9 +83,9 @@ Search: freshness=DAY 251ms 318ms 407ms -------------------------------------------------------------------------------------------------------------------- Summary: - Total test cases: 46 - Total iterations: 230 - Success rate: 230/230 (100.0%) + Total test cases: 29 + Total iterations: 145 + Success rate: 145/145 (100.0%) SDK Overhead Analysis: Average overhead: 9.2ms (3.1%) @@ -334,11 +327,11 @@ Example: ```python def test_search_with_new_filter(self, server_url, api_key, iterations, show_detailed): """Test description.""" - client = create_test_http_client("get_/v1/search") + client = create_test_http_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="test", new_filter="value") + you.search(query="test", new_filter="value") metrics = measure_sdk_call(call, client, iterations, "Search: new filter") ALL_METRICS.append(metrics) diff --git a/tests/README.md b/tests/README.md index ca38eb3..6d17868 100644 --- a/tests/README.md +++ b/tests/README.md @@ -54,11 +54,12 @@ pytest tests/ -v - `test_client.py` - Helper utilities for creating test HTTP clients - `test_search.py` - Tests for the Search API (`/v1/search`) - `test_contents.py` - Tests for the Contents API (`/v1/contents`) -- `test_runs.py` - Tests for the Agents/Runs API (`/v1/agents/runs`) +- `test_answer.py` - Tests for the Answer API (`/v1/answer`) +- `test_direct_methods.py` - Tests for direct methods on `You` (search, contents) +- `test_shims.py` - Tests for backward-compat sub-SDK shims with DeprecationWarning - `test_research.py` - Tests for the Research API (`/v1/research`) including background mode, output_schema, and source_control - `test_research_helpers.py` - Tests for the hand-maintained `research_helpers` module (background submission, polling, streaming, research_and_wait) - `test_security_env.py` - Tests for environment variable precedence (`YDC_API_KEY` / `YOU_API_KEY_AUTH`) -- `test_user_agent_hook.py` - Tests for the `YDCUserAgentOverrideHook` custom user-agent pass-through - `test_performance.py` - Performance/instrumentation tests measuring SDK overhead - `test_live.py` - Live API tests that run against the real You.com API (requires API key) @@ -66,25 +67,24 @@ pytest tests/ -v Tests are organized into logical classes using pytest: -**Search API** (9 tests): +**Search API** (10 tests): - Basic search functionality - Search with filters (freshness, country, safesearch) - Pagination and livecrawl -- News livecrawl with contents (new in 2.2.0) -- Error handling (unauthorized, forbidden) +- News livecrawl with contents +- Error handling (unauthorized, forbidden, unprocessable, internal server error) -**Contents API** (8 tests): +**Contents API** (12 tests): - HTML and Markdown format generation - Single and multiple URL processing - Optional format parameter - Error handling (unauthorized, forbidden, empty URLs) -**Agents/Runs API** (12 tests): -- Express agent (basic, streaming, with tools) -- Advanced agent (with research, compute, multiple tools) -- Custom agents (UUID-based) -- Tool configurations and verbosity -- Error handling (unauthorized, forbidden, empty input) +**Answer API** (19 tests): +- Basic answer functionality +- Answer with freshness, country, boost domains +- Async answer +- Error handling (unauthorized, forbidden, payment required, unprocessable, internal server error) **Research API**: - Basic research functionality (standard, deep, exhaustive effort) @@ -103,7 +103,7 @@ Tests are organized into logical classes using pytest: ### Running Live Tests -The `test_live.py` file contains tests that run against the real You.com API. These are skipped by default unless an API key is provided: +The `test_live.py` file contains tests that run against the real You.com API. All tests require an API key and are skipped unless `YDC_API_KEY` or `YOU_API_KEY_AUTH` is set: ```bash # Run live tests with your API key @@ -116,9 +116,7 @@ pytest tests/ --ignore=tests/test_live.py -v ## Test Coverage All tests cover the functionality demonstrated in the `examples/` directory: -- ✓ All search examples (`examples/search.py`) -- ✓ All contents examples (`examples/contents.py`) -- ✓ All agents examples (`examples/agents.py`) +- ✓ All API examples (`examples/api-example-calls.py`) Additionally, tests include: - ✓ Error response handling for all endpoints @@ -129,7 +127,7 @@ Additionally, tests include: The tests use a mock server located in `tests/mockserver/`. This server contains: -- **Auto-generated code**: Core framework from Speakeasy (`internal/sdk/`, `internal/server/`) +- **Hand-maintained Go code**: Core server framework and SDK models (`internal/sdk/`, `internal/server/`) - **Custom handlers**: Test-specific responses for success and error scenarios The mock server supports: diff --git a/tests/__init__.py b/tests/__init__.py index 368144a..ae78246 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - # tests/__init__.py diff --git a/tests/mockserver/README.md b/tests/mockserver/README.md index 972b31c..bbea551 100644 --- a/tests/mockserver/README.md +++ b/tests/mockserver/README.md @@ -64,4 +64,4 @@ docker run -i -p 18080:18080 -t --rm mockserver -log-level=DEBUG ## Code Structure - **Auto-generated**: Core mock server framework, routing, and SDK models (`internal/sdk/`, `internal/server/`, `internal/logging/`, `internal/tracking/`) -- **Custom**: Test-specific handlers with success and error scenarios for comprehensive testing (`internal/handler/pathgetv1search.go`, `internal/handler/pathpostv1contents.go`, `internal/handler/pathpostv1agentsruns.go`) +- **Custom**: Test-specific handlers with success and error scenarios for comprehensive testing (`internal/handler/pathgetv1search.go`, `internal/handler/pathpostv1contents.go`, `internal/handler/pathpostv1search.go`) diff --git a/tests/mockserver/internal/handler/assert/contenttype.go b/tests/mockserver/internal/handler/assert/contenttype.go index 42afbd0..759c096 100644 --- a/tests/mockserver/internal/handler/assert/contenttype.go +++ b/tests/mockserver/internal/handler/assert/contenttype.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package assert diff --git a/tests/mockserver/internal/handler/assert/header.go b/tests/mockserver/internal/handler/assert/header.go index 50f910d..e174ea6 100644 --- a/tests/mockserver/internal/handler/assert/header.go +++ b/tests/mockserver/internal/handler/assert/header.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package assert diff --git a/tests/mockserver/internal/handler/assert/parameter.go b/tests/mockserver/internal/handler/assert/parameter.go index f8a5e45..4d85b0d 100644 --- a/tests/mockserver/internal/handler/assert/parameter.go +++ b/tests/mockserver/internal/handler/assert/parameter.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package assert diff --git a/tests/mockserver/internal/handler/assert/pointer.go b/tests/mockserver/internal/handler/assert/pointer.go index fa03e4c..3dae423 100644 --- a/tests/mockserver/internal/handler/assert/pointer.go +++ b/tests/mockserver/internal/handler/assert/pointer.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package assert diff --git a/tests/mockserver/internal/handler/assert/security.go b/tests/mockserver/internal/handler/assert/security.go index 7d0a714..73a2d4a 100644 --- a/tests/mockserver/internal/handler/assert/security.go +++ b/tests/mockserver/internal/handler/assert/security.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package assert diff --git a/tests/mockserver/internal/handler/doc.go b/tests/mockserver/internal/handler/doc.go index a4e60c8..a0ef33d 100644 --- a/tests/mockserver/internal/handler/doc.go +++ b/tests/mockserver/internal/handler/doc.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. // Package handlers contains all generated HTTP handlers for the server. They // are listed via the generated GeneratedHandlers() function. diff --git a/tests/mockserver/internal/handler/generated_handler.go b/tests/mockserver/internal/handler/generated_handler.go index 929558e..37098bb 100644 --- a/tests/mockserver/internal/handler/generated_handler.go +++ b/tests/mockserver/internal/handler/generated_handler.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package handler diff --git a/tests/mockserver/internal/handler/generated_handlers.go b/tests/mockserver/internal/handler/generated_handlers.go index e2bdae4..eea1109 100644 --- a/tests/mockserver/internal/handler/generated_handlers.go +++ b/tests/mockserver/internal/handler/generated_handlers.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package handler @@ -12,8 +11,7 @@ import ( // GeneratedHandlers returns all generated handlers. func GeneratedHandlers(ctx context.Context, dir *logging.HTTPFileDirectory, rt *tracking.RequestTracker) []*GeneratedHandler { return []*GeneratedHandler{ - NewGeneratedHandler(ctx, http.MethodGet, "/v1/search", pathGetV1Search(dir, rt)), - NewGeneratedHandler(ctx, http.MethodPost, "/v1/agents/runs", pathPostV1AgentsRuns(dir, rt)), + NewGeneratedHandler(ctx, http.MethodPost, "/v1/search", pathPostV1Search(dir, rt)), NewGeneratedHandler(ctx, http.MethodPost, "/v1/contents", pathPostV1Contents(dir, rt)), NewGeneratedHandler(ctx, http.MethodPost, "/v1/research", pathPostV1Research(dir, rt)), NewGeneratedHandler(ctx, http.MethodGet, "/v1/research/{task_id}", pathGetV1Research(dir, rt)), diff --git a/tests/mockserver/internal/handler/pathgetv1research.go b/tests/mockserver/internal/handler/pathgetv1research.go index 781dcdf..9e3a5d4 100644 --- a/tests/mockserver/internal/handler/pathgetv1research.go +++ b/tests/mockserver/internal/handler/pathgetv1research.go @@ -12,8 +12,8 @@ import ( 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") + test := req.Header.Get("x-test-name") + instanceID := req.Header.Get("x-test-instance-id") count := rt.GetRequestCount(test, instanceID) diff --git a/tests/mockserver/internal/handler/pathgetv1researchstream.go b/tests/mockserver/internal/handler/pathgetv1researchstream.go index 4b0c57d..fba9325 100644 --- a/tests/mockserver/internal/handler/pathgetv1researchstream.go +++ b/tests/mockserver/internal/handler/pathgetv1researchstream.go @@ -12,8 +12,8 @@ import ( 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") + test := req.Header.Get("x-test-name") + instanceID := req.Header.Get("x-test-instance-id") count := rt.GetRequestCount(test, instanceID) diff --git a/tests/mockserver/internal/handler/pathgetv1search.go b/tests/mockserver/internal/handler/pathgetv1search.go deleted file mode 100644 index ade8ccd..0000000 --- a/tests/mockserver/internal/handler/pathgetv1search.go +++ /dev/null @@ -1,109 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package handler - -import ( - "fmt" - "log" - "mockserver/internal/handler/assert" - "mockserver/internal/logging" - "mockserver/internal/sdk/models/operations" - "mockserver/internal/sdk/types" - "mockserver/internal/sdk/utils" - "mockserver/internal/tracking" - "net/http" -) - -func pathGetV1Search(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/search[0]": - dir.HandlerFunc("get_/v1/search", testGetV1SearchGetV1Search0)(w, req) - case "get_/v1/search-unauthorized[0]": - testGetV1SearchUnauthorized(w, req) - case "get_/v1/search-forbidden[0]": - testGetV1SearchForbidden(w, req) - default: - dir.HandlerFunc("get_/v1/search", testGetV1SearchGetV1Search0)(w, req) - } - } -} - -func testGetV1SearchGetV1Search0(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.AcceptHeader(req, []string{"application/json"}); 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 respBody *operations.GetV1SearchResponseBody = &operations.GetV1SearchResponseBody{ - Results: &operations.Results{ - Web: []operations.Web{ - operations.Web{ - URL: types.String("https://you.com"), - Title: types.String("The World's Greatest Search Engine!"), - Description: types.String("Search on YDC"), - Snippets: []string{ - "I'm an AI assistant that helps you get more done. What can I help you with?", - }, - ThumbnailURL: types.String("https://www.somethumbnailsite.com/thumbnail.jpg"), - PageAge: types.MustNewTimeFromString("2025-06-25T11:41:00Z"), - FaviconURL: types.String("https://someurl.com/favicon"), - }, - }, - News: []operations.News{ - operations.News{ - Title: types.String("Exclusive | You.com becomes the backbone of the EU's AI strategy"), - Description: types.String("As the EU's AI strategy is being debated, You.com becomes the backbone of the EU's AI strategy."), - PageAge: types.MustNewTimeFromString("2025-06-25T11:41:00Z"), - ThumbnailURL: types.String("https://www.somethumbnailsite.com/thumbnail.jpg"), - URL: types.String("https://www.you.com/news/eu-ai-strategy-youcom"), - }, - }, - }, - Metadata: &operations.Metadata{ - SearchUUID: types.String("942ccbdd-7705-4d9c-9d37-4ef386658e90"), - Query: types.String("Your query"), - Latency: types.Float64(0.123), - }, - } - respBodyBytes, err := utils.MarshalJSON(respBody, "", true) - - 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 testGetV1SearchUnauthorized(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 testGetV1SearchForbidden(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) -} diff --git a/tests/mockserver/internal/handler/pathpostv1agentsruns.go b/tests/mockserver/internal/handler/pathpostv1agentsruns.go deleted file mode 100644 index 0347e0c..0000000 --- a/tests/mockserver/internal/handler/pathpostv1agentsruns.go +++ /dev/null @@ -1,131 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package handler - -import ( - "encoding/json" - "fmt" - "io" - "log" - "mockserver/internal/handler/assert" - "mockserver/internal/logging" - "mockserver/internal/tracking" - "net/http" -) - -func pathPostV1AgentsRuns(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/agents/runs[0]": - dir.HandlerFunc("post_/v1/agents/runs", testPostV1AgentsRunsPostV1AgentsRuns0)(w, req) - case "post_/v1/agents/runs-unauthorized[0]": - testPostV1AgentsRunsUnauthorized(w, req) - case "post_/v1/agents/runs-forbidden[0]": - testPostV1AgentsRunsForbidden(w, req) - case "post_/v1/agents/runs-bad-request[0]": - testPostV1AgentsRunsBadRequest(w, req) - default: - dir.HandlerFunc("post_/v1/agents/runs", testPostV1AgentsRunsPostV1AgentsRuns0)(w, req) - } - } -} - -func testPostV1AgentsRunsUnauthorized(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(`{"errors":[{"status":"401","code":"unauthorized","title":"Unauthorized","detail":"Invalid or expired API key"}]}`)) -} - -func testPostV1AgentsRunsForbidden(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusForbidden) - _, _ = w.Write([]byte(`{"errors":[{"status":"403","code":"forbidden","title":"Forbidden","detail":"API key lacks scope for this path"}]}`)) -} - -func testPostV1AgentsRunsBadRequest(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(`{"detail":"Bad request"}`)) -} - -func testPostV1AgentsRunsPostV1AgentsRuns0(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 - } - // Accept header check removed - SDK might send different values - if err := assert.HeaderExists(req, "User-Agent"); err != nil { - log.Printf("assertion error: %s\n", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Parse request body to get agent and input - 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 - } - - agent := "express" - if agentStr, ok := requestBody["agent"].(string); ok { - agent = agentStr - } - - input := "default input" - if inputStr, ok := requestBody["input"].(string); ok { - input = inputStr - } - - // Generate response text based on input - responseText := "This is a mock response to: " + input - - // Construct response with correct structure matching current SDK expectations - respMap := map[string]interface{}{ - "agent": agent, - "input": []map[string]interface{}{ - { - "role": "user", - "content": input, - }, - }, - "output": []map[string]interface{}{ - { - "type": "message.answer", - "text": responseText, - }, - }, - } - - respBodyBytes, err := json.Marshal(respMap) - 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) -} - diff --git a/tests/mockserver/internal/handler/pathpostv1contents.go b/tests/mockserver/internal/handler/pathpostv1contents.go index de91ec1..e839d59 100644 --- a/tests/mockserver/internal/handler/pathpostv1contents.go +++ b/tests/mockserver/internal/handler/pathpostv1contents.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package handler @@ -18,8 +17,8 @@ import ( func pathPostV1Contents(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") + test := req.Header.Get("x-test-name") + instanceID := req.Header.Get("x-test-instance-id") count := rt.GetRequestCount(test, instanceID) diff --git a/tests/mockserver/internal/handler/pathpostv1financeresearch.go b/tests/mockserver/internal/handler/pathpostv1financeresearch.go index 64c3b81..62941a1 100644 --- a/tests/mockserver/internal/handler/pathpostv1financeresearch.go +++ b/tests/mockserver/internal/handler/pathpostv1financeresearch.go @@ -13,8 +13,8 @@ import ( 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") + test := req.Header.Get("x-test-name") + instanceID := req.Header.Get("x-test-instance-id") count := rt.GetRequestCount(test, instanceID) diff --git a/tests/mockserver/internal/handler/pathpostv1research.go b/tests/mockserver/internal/handler/pathpostv1research.go index 3b8d857..2c07ef5 100644 --- a/tests/mockserver/internal/handler/pathpostv1research.go +++ b/tests/mockserver/internal/handler/pathpostv1research.go @@ -20,8 +20,8 @@ import ( // so the SDK can assert round-trip through `Union[str, Dict]` Content. func pathPostV1Research(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") + test := req.Header.Get("x-test-name") + instanceID := req.Header.Get("x-test-instance-id") count := rt.GetRequestCount(test, instanceID) diff --git a/tests/mockserver/internal/handler/pathpostv1search.go b/tests/mockserver/internal/handler/pathpostv1search.go new file mode 100644 index 0000000..748973f --- /dev/null +++ b/tests/mockserver/internal/handler/pathpostv1search.go @@ -0,0 +1,56 @@ +package handler + +import ( + "log" + "mockserver/internal/handler/assert" + "mockserver/internal/logging" + "mockserver/internal/tracking" + "net/http" +) + +func pathPostV1Search(dir *logging.HTTPFileDirectory, rt *tracking.RequestTracker) http.HandlerFunc { + return func(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 + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "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."], + "thumbnail_url": "https://www.somethumbnailsite.com/thumbnail.jpg", + "page_age": "2025-06-25T11:41:00Z", + "favicon_url": "https://someurl.com/favicon" + } + ], + "news": [ + { + "title": "You.com becomes the backbone of the EU's AI strategy", + "description": "You.com becomes the backbone of the EU's AI strategy.", + "page_age": "2025-06-25T11:41:00Z", + "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 + } + }`)) + } +} diff --git a/tests/mockserver/internal/handler/values/files.go b/tests/mockserver/internal/handler/values/files.go index 6eca885..57122eb 100644 --- a/tests/mockserver/internal/handler/values/files.go +++ b/tests/mockserver/internal/handler/values/files.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package values diff --git a/tests/mockserver/internal/logging/doc.go b/tests/mockserver/internal/logging/doc.go index 8835f1c..64d41b9 100644 --- a/tests/mockserver/internal/logging/doc.go +++ b/tests/mockserver/internal/logging/doc.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. // Package logging contains the implementation and helpers for logging. package logging diff --git a/tests/mockserver/internal/logging/formats.go b/tests/mockserver/internal/logging/formats.go index d6ab53c..750c410 100644 --- a/tests/mockserver/internal/logging/formats.go +++ b/tests/mockserver/internal/logging/formats.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package logging diff --git a/tests/mockserver/internal/logging/http_file.go b/tests/mockserver/internal/logging/http_file.go index d690d91..853d6a7 100644 --- a/tests/mockserver/internal/logging/http_file.go +++ b/tests/mockserver/internal/logging/http_file.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package logging diff --git a/tests/mockserver/internal/logging/http_logger.go b/tests/mockserver/internal/logging/http_logger.go index fdd5d05..6af6453 100644 --- a/tests/mockserver/internal/logging/http_logger.go +++ b/tests/mockserver/internal/logging/http_logger.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package logging diff --git a/tests/mockserver/internal/logging/levels.go b/tests/mockserver/internal/logging/levels.go index d2a8e93..18f8c04 100644 --- a/tests/mockserver/internal/logging/levels.go +++ b/tests/mockserver/internal/logging/levels.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package logging diff --git a/tests/mockserver/internal/logging/logger.go b/tests/mockserver/internal/logging/logger.go index bfd6dc7..fb16dff 100644 --- a/tests/mockserver/internal/logging/logger.go +++ b/tests/mockserver/internal/logging/logger.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package logging diff --git a/tests/mockserver/internal/logging/oas_operation.go b/tests/mockserver/internal/logging/oas_operation.go index d4f2af7..3c4cdfd 100644 --- a/tests/mockserver/internal/logging/oas_operation.go +++ b/tests/mockserver/internal/logging/oas_operation.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package logging diff --git a/tests/mockserver/internal/logging/oas_operation_call.go b/tests/mockserver/internal/logging/oas_operation_call.go index beb0590..6efbf53 100644 --- a/tests/mockserver/internal/logging/oas_operation_call.go +++ b/tests/mockserver/internal/logging/oas_operation_call.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package logging diff --git a/tests/mockserver/internal/sdk/models/components/agenttype.go b/tests/mockserver/internal/sdk/models/components/agenttype.go deleted file mode 100644 index 5a992d0..0000000 --- a/tests/mockserver/internal/sdk/models/components/agenttype.go +++ /dev/null @@ -1,35 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "encoding/json" - "fmt" -) - -// AgentType - Built-in agent types -type AgentType string - -const ( - AgentTypeExpress AgentType = "express" - AgentTypeAdvanced AgentType = "advanced" -) - -func (e AgentType) ToPointer() *AgentType { - return &e -} -func (e *AgentType) UnmarshalJSON(data []byte) error { - var v string - if err := json.Unmarshal(data, &v); err != nil { - return err - } - switch v { - case "express": - fallthrough - case "advanced": - *e = AgentType(v) - return nil - default: - return fmt.Errorf("invalid value for AgentType: %v", v) - } -} diff --git a/tests/mockserver/internal/sdk/models/components/chatanswerfull.go b/tests/mockserver/internal/sdk/models/components/chatanswerfull.go deleted file mode 100644 index e4ab375..0000000 --- a/tests/mockserver/internal/sdk/models/components/chatanswerfull.go +++ /dev/null @@ -1,72 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "mockserver/internal/sdk/utils" -) - -type ChatAnswerFullSource struct { - URL *string `json:"url,omitempty"` - Title *string `json:"title,omitempty"` -} - -func (c ChatAnswerFullSource) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(c, "", false) -} - -func (c *ChatAnswerFullSource) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { - return err - } - return nil -} - -func (o *ChatAnswerFullSource) GetURL() *string { - if o == nil { - return nil - } - return o.URL -} - -func (o *ChatAnswerFullSource) GetTitle() *string { - if o == nil { - return nil - } - return o.Title -} - -type ChatAnswerFull struct { - type_ string `const:"message.answer" json:"type"` - Text *string `json:"text,omitempty"` - Sources []ChatAnswerFullSource `json:"sources,omitempty"` -} - -func (c ChatAnswerFull) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(c, "", false) -} - -func (c *ChatAnswerFull) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &c, "", false, []string{"type"}); err != nil { - return err - } - return nil -} - -func (o *ChatAnswerFull) GetType() string { - return "message.answer" -} - -func (o *ChatAnswerFull) GetText() *string { - if o == nil { - return nil - } - return o.Text -} - -func (o *ChatAnswerFull) GetSources() []ChatAnswerFullSource { - if o == nil { - return nil - } - return o.Sources -} diff --git a/tests/mockserver/internal/sdk/models/components/computeresultsfull.go b/tests/mockserver/internal/sdk/models/components/computeresultsfull.go deleted file mode 100644 index 3131aaa..0000000 --- a/tests/mockserver/internal/sdk/models/components/computeresultsfull.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "mockserver/internal/sdk/utils" -) - -type ComputeResultsFull struct { - type_ string `const:"compute.results" json:"type"` - Result *string `json:"result,omitempty"` - Expression *string `json:"expression,omitempty"` -} - -func (c ComputeResultsFull) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(c, "", false) -} - -func (c *ComputeResultsFull) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &c, "", false, []string{"type"}); err != nil { - return err - } - return nil -} - -func (o *ComputeResultsFull) GetType() string { - return "compute.results" -} - -func (o *ComputeResultsFull) GetResult() *string { - if o == nil { - return nil - } - return o.Result -} - -func (o *ComputeResultsFull) GetExpression() *string { - if o == nil { - return nil - } - return o.Expression -} diff --git a/tests/mockserver/internal/sdk/models/components/computetool.go b/tests/mockserver/internal/sdk/models/components/computetool.go deleted file mode 100644 index 9790622..0000000 --- a/tests/mockserver/internal/sdk/models/components/computetool.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "mockserver/internal/sdk/utils" -) - -// ComputeTool - Mathematical computation tool -type ComputeTool struct { - type_ string `const:"compute" json:"type"` -} - -func (c ComputeTool) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(c, "", false) -} - -func (c *ComputeTool) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &c, "", false, []string{"type"}); err != nil { - return err - } - return nil -} - -func (o *ComputeTool) GetType() string { - return "compute" -} diff --git a/tests/mockserver/internal/sdk/models/components/country.go b/tests/mockserver/internal/sdk/models/components/country.go index a77e822..c6b505f 100644 --- a/tests/mockserver/internal/sdk/models/components/country.go +++ b/tests/mockserver/internal/sdk/models/components/country.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package components diff --git a/tests/mockserver/internal/sdk/models/components/freshness.go b/tests/mockserver/internal/sdk/models/components/freshness.go index 66a0f58..a1a101f 100644 --- a/tests/mockserver/internal/sdk/models/components/freshness.go +++ b/tests/mockserver/internal/sdk/models/components/freshness.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package components diff --git a/tests/mockserver/internal/sdk/models/components/fullresponse.go b/tests/mockserver/internal/sdk/models/components/fullresponse.go deleted file mode 100644 index 031af41..0000000 --- a/tests/mockserver/internal/sdk/models/components/fullresponse.go +++ /dev/null @@ -1,182 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "errors" - "fmt" - "mockserver/internal/sdk/utils" -) - -type FullResponseType string - -const ( - FullResponseTypeWebSearchResultsFull FullResponseType = "WebSearchResultsFull" - FullResponseTypeChatAnswerFull FullResponseType = "ChatAnswerFull" - FullResponseTypeResearchResultsFull FullResponseType = "ResearchResultsFull" - FullResponseTypeComputeResultsFull FullResponseType = "ComputeResultsFull" - FullResponseTypeLiveCrawlResultsFull FullResponseType = "LiveCrawlResultsFull" - FullResponseTypeGenericFull FullResponseType = "GenericFull" - FullResponseTypeArrayOfMapOfAny FullResponseType = "arrayOfMapOfAny" -) - -type FullResponse struct { - WebSearchResultsFull *WebSearchResultsFull `queryParam:"inline"` - ChatAnswerFull *ChatAnswerFull `queryParam:"inline"` - ResearchResultsFull *ResearchResultsFull `queryParam:"inline"` - ComputeResultsFull *ComputeResultsFull `queryParam:"inline"` - LiveCrawlResultsFull *LiveCrawlResultsFull `queryParam:"inline"` - GenericFull *GenericFull `queryParam:"inline"` - ArrayOfMapOfAny []map[string]any `queryParam:"inline"` - - Type FullResponseType -} - -func CreateFullResponseWebSearchResultsFull(webSearchResultsFull WebSearchResultsFull) FullResponse { - typ := FullResponseTypeWebSearchResultsFull - - return FullResponse{ - WebSearchResultsFull: &webSearchResultsFull, - Type: typ, - } -} - -func CreateFullResponseChatAnswerFull(chatAnswerFull ChatAnswerFull) FullResponse { - typ := FullResponseTypeChatAnswerFull - - return FullResponse{ - ChatAnswerFull: &chatAnswerFull, - Type: typ, - } -} - -func CreateFullResponseResearchResultsFull(researchResultsFull ResearchResultsFull) FullResponse { - typ := FullResponseTypeResearchResultsFull - - return FullResponse{ - ResearchResultsFull: &researchResultsFull, - Type: typ, - } -} - -func CreateFullResponseComputeResultsFull(computeResultsFull ComputeResultsFull) FullResponse { - typ := FullResponseTypeComputeResultsFull - - return FullResponse{ - ComputeResultsFull: &computeResultsFull, - Type: typ, - } -} - -func CreateFullResponseLiveCrawlResultsFull(liveCrawlResultsFull LiveCrawlResultsFull) FullResponse { - typ := FullResponseTypeLiveCrawlResultsFull - - return FullResponse{ - LiveCrawlResultsFull: &liveCrawlResultsFull, - Type: typ, - } -} - -func CreateFullResponseGenericFull(genericFull GenericFull) FullResponse { - typ := FullResponseTypeGenericFull - - return FullResponse{ - GenericFull: &genericFull, - Type: typ, - } -} - -func CreateFullResponseArrayOfMapOfAny(arrayOfMapOfAny []map[string]any) FullResponse { - typ := FullResponseTypeArrayOfMapOfAny - - return FullResponse{ - ArrayOfMapOfAny: arrayOfMapOfAny, - Type: typ, - } -} - -func (u *FullResponse) UnmarshalJSON(data []byte) error { - - var webSearchResultsFull WebSearchResultsFull = WebSearchResultsFull{} - if err := utils.UnmarshalJSON(data, &webSearchResultsFull, "", true, nil); err == nil { - u.WebSearchResultsFull = &webSearchResultsFull - u.Type = FullResponseTypeWebSearchResultsFull - return nil - } - - var chatAnswerFull ChatAnswerFull = ChatAnswerFull{} - if err := utils.UnmarshalJSON(data, &chatAnswerFull, "", true, nil); err == nil { - u.ChatAnswerFull = &chatAnswerFull - u.Type = FullResponseTypeChatAnswerFull - return nil - } - - var researchResultsFull ResearchResultsFull = ResearchResultsFull{} - if err := utils.UnmarshalJSON(data, &researchResultsFull, "", true, nil); err == nil { - u.ResearchResultsFull = &researchResultsFull - u.Type = FullResponseTypeResearchResultsFull - return nil - } - - var computeResultsFull ComputeResultsFull = ComputeResultsFull{} - if err := utils.UnmarshalJSON(data, &computeResultsFull, "", true, nil); err == nil { - u.ComputeResultsFull = &computeResultsFull - u.Type = FullResponseTypeComputeResultsFull - return nil - } - - var liveCrawlResultsFull LiveCrawlResultsFull = LiveCrawlResultsFull{} - if err := utils.UnmarshalJSON(data, &liveCrawlResultsFull, "", true, nil); err == nil { - u.LiveCrawlResultsFull = &liveCrawlResultsFull - u.Type = FullResponseTypeLiveCrawlResultsFull - return nil - } - - var genericFull GenericFull = GenericFull{} - if err := utils.UnmarshalJSON(data, &genericFull, "", true, nil); err == nil { - u.GenericFull = &genericFull - u.Type = FullResponseTypeGenericFull - return nil - } - - var arrayOfMapOfAny []map[string]any = []map[string]any{} - if err := utils.UnmarshalJSON(data, &arrayOfMapOfAny, "", true, nil); err == nil { - u.ArrayOfMapOfAny = arrayOfMapOfAny - u.Type = FullResponseTypeArrayOfMapOfAny - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for FullResponse", string(data)) -} - -func (u FullResponse) MarshalJSON() ([]byte, error) { - if u.WebSearchResultsFull != nil { - return utils.MarshalJSON(u.WebSearchResultsFull, "", true) - } - - if u.ChatAnswerFull != nil { - return utils.MarshalJSON(u.ChatAnswerFull, "", true) - } - - if u.ResearchResultsFull != nil { - return utils.MarshalJSON(u.ResearchResultsFull, "", true) - } - - if u.ComputeResultsFull != nil { - return utils.MarshalJSON(u.ComputeResultsFull, "", true) - } - - if u.LiveCrawlResultsFull != nil { - return utils.MarshalJSON(u.LiveCrawlResultsFull, "", true) - } - - if u.GenericFull != nil { - return utils.MarshalJSON(u.GenericFull, "", true) - } - - if u.ArrayOfMapOfAny != nil { - return utils.MarshalJSON(u.ArrayOfMapOfAny, "", true) - } - - return nil, errors.New("could not marshal union type FullResponse: all fields are null") -} diff --git a/tests/mockserver/internal/sdk/models/components/genericfull.go b/tests/mockserver/internal/sdk/models/components/genericfull.go deleted file mode 100644 index 3e05db1..0000000 --- a/tests/mockserver/internal/sdk/models/components/genericfull.go +++ /dev/null @@ -1,38 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "mockserver/internal/sdk/utils" -) - -// GenericFull - Generic full response for unknown or custom types -type GenericFull struct { - Type *string `json:"type,omitempty"` - AdditionalProperties map[string]any `additionalProperties:"true" json:"-"` -} - -func (g GenericFull) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(g, "", false) -} - -func (g *GenericFull) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &g, "", false, nil); err != nil { - return err - } - return nil -} - -func (o *GenericFull) GetType() *string { - if o == nil { - return nil - } - return o.Type -} - -func (o *GenericFull) GetAdditionalProperties() map[string]any { - if o == nil { - return nil - } - return o.AdditionalProperties -} diff --git a/tests/mockserver/internal/sdk/models/components/httpmetadata.go b/tests/mockserver/internal/sdk/models/components/httpmetadata.go index e18bdc0..8b07013 100644 --- a/tests/mockserver/internal/sdk/models/components/httpmetadata.go +++ b/tests/mockserver/internal/sdk/models/components/httpmetadata.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package components diff --git a/tests/mockserver/internal/sdk/models/components/livecrawl.go b/tests/mockserver/internal/sdk/models/components/livecrawl.go index 8f633c2..774a510 100644 --- a/tests/mockserver/internal/sdk/models/components/livecrawl.go +++ b/tests/mockserver/internal/sdk/models/components/livecrawl.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package components diff --git a/tests/mockserver/internal/sdk/models/components/livecrawlformats.go b/tests/mockserver/internal/sdk/models/components/livecrawlformats.go index 4414884..8aea4d6 100644 --- a/tests/mockserver/internal/sdk/models/components/livecrawlformats.go +++ b/tests/mockserver/internal/sdk/models/components/livecrawlformats.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package components diff --git a/tests/mockserver/internal/sdk/models/components/livecrawlresultsfull.go b/tests/mockserver/internal/sdk/models/components/livecrawlresultsfull.go deleted file mode 100644 index 3030e3a..0000000 --- a/tests/mockserver/internal/sdk/models/components/livecrawlresultsfull.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "mockserver/internal/sdk/utils" -) - -type LiveCrawlResultsFull struct { - type_ string `const:"live_crawl.results" json:"type"` - Content map[string]any `json:"content,omitempty"` - URL *string `json:"url,omitempty"` -} - -func (l LiveCrawlResultsFull) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(l, "", false) -} - -func (l *LiveCrawlResultsFull) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &l, "", false, []string{"type"}); err != nil { - return err - } - return nil -} - -func (o *LiveCrawlResultsFull) GetType() string { - return "live_crawl.results" -} - -func (o *LiveCrawlResultsFull) GetContent() map[string]any { - if o == nil { - return nil - } - return o.Content -} - -func (o *LiveCrawlResultsFull) GetURL() *string { - if o == nil { - return nil - } - return o.URL -} diff --git a/tests/mockserver/internal/sdk/models/components/researchresultsfull.go b/tests/mockserver/internal/sdk/models/components/researchresultsfull.go deleted file mode 100644 index ac5fce4..0000000 --- a/tests/mockserver/internal/sdk/models/components/researchresultsfull.go +++ /dev/null @@ -1,80 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "mockserver/internal/sdk/utils" -) - -type ResearchResultsFullSource struct { - URL *string `json:"url,omitempty"` - Title *string `json:"title,omitempty"` - Snippet *string `json:"snippet,omitempty"` -} - -func (r ResearchResultsFullSource) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(r, "", false) -} - -func (r *ResearchResultsFullSource) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { - return err - } - return nil -} - -func (o *ResearchResultsFullSource) GetURL() *string { - if o == nil { - return nil - } - return o.URL -} - -func (o *ResearchResultsFullSource) GetTitle() *string { - if o == nil { - return nil - } - return o.Title -} - -func (o *ResearchResultsFullSource) GetSnippet() *string { - if o == nil { - return nil - } - return o.Snippet -} - -type ResearchResultsFull struct { - type_ string `const:"research.results" json:"type"` - Report *string `json:"report,omitempty"` - Sources []ResearchResultsFullSource `json:"sources,omitempty"` -} - -func (r ResearchResultsFull) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(r, "", false) -} - -func (r *ResearchResultsFull) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &r, "", false, []string{"type"}); err != nil { - return err - } - return nil -} - -func (o *ResearchResultsFull) GetType() string { - return "research.results" -} - -func (o *ResearchResultsFull) GetReport() *string { - if o == nil { - return nil - } - return o.Report -} - -func (o *ResearchResultsFull) GetSources() []ResearchResultsFullSource { - if o == nil { - return nil - } - return o.Sources -} diff --git a/tests/mockserver/internal/sdk/models/components/researchtool.go b/tests/mockserver/internal/sdk/models/components/researchtool.go deleted file mode 100644 index 70a5369..0000000 --- a/tests/mockserver/internal/sdk/models/components/researchtool.go +++ /dev/null @@ -1,173 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "errors" - "fmt" - "mockserver/internal/sdk/utils" -) - -type SearchEffortType string - -const ( - SearchEffortTypeSearchEffortEnum SearchEffortType = "SearchEffort_enum" - SearchEffortTypeStr SearchEffortType = "str" -) - -// SearchEffort - Search effort level for research: 'auto' lets agent decide -type SearchEffort struct { - SearchEffortEnum *SearchEffortEnum `queryParam:"inline"` - Str *string `queryParam:"inline"` - - Type SearchEffortType -} - -func CreateSearchEffortSearchEffortEnum(searchEffortEnum SearchEffortEnum) SearchEffort { - typ := SearchEffortTypeSearchEffortEnum - - return SearchEffort{ - SearchEffortEnum: &searchEffortEnum, - Type: typ, - } -} - -func CreateSearchEffortStr(str string) SearchEffort { - typ := SearchEffortTypeStr - - return SearchEffort{ - Str: &str, - Type: typ, - } -} - -func (u *SearchEffort) UnmarshalJSON(data []byte) error { - - var searchEffortEnum SearchEffortEnum = SearchEffortEnum("") - if err := utils.UnmarshalJSON(data, &searchEffortEnum, "", true, nil); err == nil { - u.SearchEffortEnum = &searchEffortEnum - u.Type = SearchEffortTypeSearchEffortEnum - return nil - } - - var str string = "" - if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { - u.Str = &str - u.Type = SearchEffortTypeStr - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for SearchEffort", string(data)) -} - -func (u SearchEffort) MarshalJSON() ([]byte, error) { - if u.SearchEffortEnum != nil { - return utils.MarshalJSON(u.SearchEffortEnum, "", true) - } - - if u.Str != nil { - return utils.MarshalJSON(u.Str, "", true) - } - - return nil, errors.New("could not marshal union type SearchEffort: all fields are null") -} - -type ReportVerbosityType string - -const ( - ReportVerbosityTypeVerbosity ReportVerbosityType = "Verbosity" - ReportVerbosityTypeStr ReportVerbosityType = "str" -) - -type ReportVerbosity struct { - Verbosity *Verbosity `queryParam:"inline"` - Str *string `queryParam:"inline"` - - Type ReportVerbosityType -} - -func CreateReportVerbosityVerbosity(verbosity Verbosity) ReportVerbosity { - typ := ReportVerbosityTypeVerbosity - - return ReportVerbosity{ - Verbosity: &verbosity, - Type: typ, - } -} - -func CreateReportVerbosityStr(str string) ReportVerbosity { - typ := ReportVerbosityTypeStr - - return ReportVerbosity{ - Str: &str, - Type: typ, - } -} - -func (u *ReportVerbosity) UnmarshalJSON(data []byte) error { - - var verbosity Verbosity = Verbosity("") - if err := utils.UnmarshalJSON(data, &verbosity, "", true, nil); err == nil { - u.Verbosity = &verbosity - u.Type = ReportVerbosityTypeVerbosity - return nil - } - - var str string = "" - if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { - u.Str = &str - u.Type = ReportVerbosityTypeStr - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for ReportVerbosity", string(data)) -} - -func (u ReportVerbosity) MarshalJSON() ([]byte, error) { - if u.Verbosity != nil { - return utils.MarshalJSON(u.Verbosity, "", true) - } - - if u.Str != nil { - return utils.MarshalJSON(u.Str, "", true) - } - - return nil, errors.New("could not marshal union type ReportVerbosity: all fields are null") -} - -// ResearchTool - Research tool with configurable search effort and report verbosity -type ResearchTool struct { - type_ string `const:"research" json:"type"` - // Search effort level for research: 'auto' lets agent decide - SearchEffort *SearchEffort `json:"search_effort,omitempty"` - ReportVerbosity *ReportVerbosity `json:"report_verbosity,omitempty"` -} - -func (r ResearchTool) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(r, "", false) -} - -func (r *ResearchTool) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &r, "", false, []string{"type"}); err != nil { - return err - } - return nil -} - -func (o *ResearchTool) GetType() string { - return "research" -} - -func (o *ResearchTool) GetSearchEffort() *SearchEffort { - if o == nil { - return nil - } - return o.SearchEffort -} - -func (o *ResearchTool) GetReportVerbosity() *ReportVerbosity { - if o == nil { - return nil - } - return o.ReportVerbosity -} diff --git a/tests/mockserver/internal/sdk/models/components/safesearch.go b/tests/mockserver/internal/sdk/models/components/safesearch.go index 363fc68..1466a8e 100644 --- a/tests/mockserver/internal/sdk/models/components/safesearch.go +++ b/tests/mockserver/internal/sdk/models/components/safesearch.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package components diff --git a/tests/mockserver/internal/sdk/models/components/searcheffortenum.go b/tests/mockserver/internal/sdk/models/components/searcheffortenum.go deleted file mode 100644 index 3d0201b..0000000 --- a/tests/mockserver/internal/sdk/models/components/searcheffortenum.go +++ /dev/null @@ -1,41 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "encoding/json" - "fmt" -) - -// SearchEffortEnum - Search effort level for research: 'auto' lets agent decide -type SearchEffortEnum string - -const ( - SearchEffortEnumLow SearchEffortEnum = "low" - SearchEffortEnumMedium SearchEffortEnum = "medium" - SearchEffortEnumHigh SearchEffortEnum = "high" - SearchEffortEnumAuto SearchEffortEnum = "auto" -) - -func (e SearchEffortEnum) ToPointer() *SearchEffortEnum { - return &e -} -func (e *SearchEffortEnum) UnmarshalJSON(data []byte) error { - var v string - if err := json.Unmarshal(data, &v); err != nil { - return err - } - switch v { - case "low": - fallthrough - case "medium": - fallthrough - case "high": - fallthrough - case "auto": - *e = SearchEffortEnum(v) - return nil - default: - return fmt.Errorf("invalid value for SearchEffortEnum: %v", v) - } -} diff --git a/tests/mockserver/internal/sdk/models/components/security.go b/tests/mockserver/internal/sdk/models/components/security.go index 78967c1..d3e5a51 100644 --- a/tests/mockserver/internal/sdk/models/components/security.go +++ b/tests/mockserver/internal/sdk/models/components/security.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package components diff --git a/tests/mockserver/internal/sdk/models/components/tool.go b/tests/mockserver/internal/sdk/models/components/tool.go deleted file mode 100644 index 88800bb..0000000 --- a/tests/mockserver/internal/sdk/models/components/tool.go +++ /dev/null @@ -1,113 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "encoding/json" - "errors" - "fmt" - "mockserver/internal/sdk/utils" -) - -type ToolType string - -const ( - ToolTypeWebSearch ToolType = "web_search" - ToolTypeResearch ToolType = "research" - ToolTypeCompute ToolType = "compute" -) - -type Tool struct { - WebSearchTool *WebSearchTool `queryParam:"inline"` - ResearchTool *ResearchTool `queryParam:"inline"` - ComputeTool *ComputeTool `queryParam:"inline"` - - Type ToolType -} - -func CreateToolWebSearch(webSearch WebSearchTool) Tool { - typ := ToolTypeWebSearch - - return Tool{ - WebSearchTool: &webSearch, - Type: typ, - } -} - -func CreateToolResearch(research ResearchTool) Tool { - typ := ToolTypeResearch - - return Tool{ - ResearchTool: &research, - Type: typ, - } -} - -func CreateToolCompute(compute ComputeTool) Tool { - typ := ToolTypeCompute - - return Tool{ - ComputeTool: &compute, - Type: typ, - } -} - -func (u *Tool) UnmarshalJSON(data []byte) error { - - type discriminator struct { - Type string `json:"type"` - } - - dis := new(discriminator) - if err := json.Unmarshal(data, &dis); err != nil { - return fmt.Errorf("could not unmarshal discriminator: %w", err) - } - - switch dis.Type { - case "web_search": - webSearchTool := new(WebSearchTool) - if err := utils.UnmarshalJSON(data, &webSearchTool, "", true, nil); err != nil { - return fmt.Errorf("could not unmarshal `%s` into expected (Type == web_search) type WebSearchTool within Tool: %w", string(data), err) - } - - u.WebSearchTool = webSearchTool - u.Type = ToolTypeWebSearch - return nil - case "research": - researchTool := new(ResearchTool) - if err := utils.UnmarshalJSON(data, &researchTool, "", true, nil); err != nil { - return fmt.Errorf("could not unmarshal `%s` into expected (Type == research) type ResearchTool within Tool: %w", string(data), err) - } - - u.ResearchTool = researchTool - u.Type = ToolTypeResearch - return nil - case "compute": - computeTool := new(ComputeTool) - if err := utils.UnmarshalJSON(data, &computeTool, "", true, nil); err != nil { - return fmt.Errorf("could not unmarshal `%s` into expected (Type == compute) type ComputeTool within Tool: %w", string(data), err) - } - - u.ComputeTool = computeTool - u.Type = ToolTypeCompute - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for Tool", string(data)) -} - -func (u Tool) MarshalJSON() ([]byte, error) { - if u.WebSearchTool != nil { - return utils.MarshalJSON(u.WebSearchTool, "", true) - } - - if u.ResearchTool != nil { - return utils.MarshalJSON(u.ResearchTool, "", true) - } - - if u.ComputeTool != nil { - return utils.MarshalJSON(u.ComputeTool, "", true) - } - - return nil, errors.New("could not marshal union type Tool: all fields are null") -} diff --git a/tests/mockserver/internal/sdk/models/components/trigger.go b/tests/mockserver/internal/sdk/models/components/trigger.go deleted file mode 100644 index f0d7b11..0000000 --- a/tests/mockserver/internal/sdk/models/components/trigger.go +++ /dev/null @@ -1,35 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "encoding/json" - "fmt" -) - -// Trigger - Tool trigger mode: 'intent' lets agent decide, 'force' always uses the tool -type Trigger string - -const ( - TriggerIntent Trigger = "intent" - TriggerForce Trigger = "force" -) - -func (e Trigger) ToPointer() *Trigger { - return &e -} -func (e *Trigger) UnmarshalJSON(data []byte) error { - var v string - if err := json.Unmarshal(data, &v); err != nil { - return err - } - switch v { - case "intent": - fallthrough - case "force": - *e = Trigger(v) - return nil - default: - return fmt.Errorf("invalid value for Trigger: %v", v) - } -} diff --git a/tests/mockserver/internal/sdk/models/components/verbosity.go b/tests/mockserver/internal/sdk/models/components/verbosity.go deleted file mode 100644 index 243a4fa..0000000 --- a/tests/mockserver/internal/sdk/models/components/verbosity.go +++ /dev/null @@ -1,38 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "encoding/json" - "fmt" -) - -// Verbosity - Response verbosity level -type Verbosity string - -const ( - VerbosityLow Verbosity = "low" - VerbosityMedium Verbosity = "medium" - VerbosityHigh Verbosity = "high" -) - -func (e Verbosity) ToPointer() *Verbosity { - return &e -} -func (e *Verbosity) UnmarshalJSON(data []byte) error { - var v string - if err := json.Unmarshal(data, &v); err != nil { - return err - } - switch v { - case "low": - fallthrough - case "medium": - fallthrough - case "high": - *e = Verbosity(v) - return nil - default: - return fmt.Errorf("invalid value for Verbosity: %v", v) - } -} diff --git a/tests/mockserver/internal/sdk/models/components/websearchresultsfull.go b/tests/mockserver/internal/sdk/models/components/websearchresultsfull.go deleted file mode 100644 index 32be597..0000000 --- a/tests/mockserver/internal/sdk/models/components/websearchresultsfull.go +++ /dev/null @@ -1,80 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "mockserver/internal/sdk/utils" -) - -type Result struct { - URL *string `json:"url,omitempty"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` - Age *string `json:"age,omitempty"` -} - -func (r Result) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(r, "", false) -} - -func (r *Result) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &r, "", false, nil); err != nil { - return err - } - return nil -} - -func (o *Result) GetURL() *string { - if o == nil { - return nil - } - return o.URL -} - -func (o *Result) GetTitle() *string { - if o == nil { - return nil - } - return o.Title -} - -func (o *Result) GetDescription() *string { - if o == nil { - return nil - } - return o.Description -} - -func (o *Result) GetAge() *string { - if o == nil { - return nil - } - return o.Age -} - -type WebSearchResultsFull struct { - type_ string `const:"web_search.results" json:"type"` - Results []Result `json:"results,omitempty"` -} - -func (w WebSearchResultsFull) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(w, "", false) -} - -func (w *WebSearchResultsFull) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &w, "", false, []string{"type"}); err != nil { - return err - } - return nil -} - -func (o *WebSearchResultsFull) GetType() string { - return "web_search.results" -} - -func (o *WebSearchResultsFull) GetResults() []Result { - if o == nil { - return nil - } - return o.Results -} diff --git a/tests/mockserver/internal/sdk/models/components/websearchtool.go b/tests/mockserver/internal/sdk/models/components/websearchtool.go deleted file mode 100644 index 4edfd24..0000000 --- a/tests/mockserver/internal/sdk/models/components/websearchtool.go +++ /dev/null @@ -1,36 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package components - -import ( - "mockserver/internal/sdk/utils" -) - -// WebSearchTool - Web search tool with optional intent-based triggering -type WebSearchTool struct { - type_ string `const:"web_search" json:"type"` - // Tool trigger mode: 'intent' lets agent decide, 'force' always uses the tool - Trigger *Trigger `json:"trigger,omitempty"` -} - -func (w WebSearchTool) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(w, "", false) -} - -func (w *WebSearchTool) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &w, "", false, []string{"type"}); err != nil { - return err - } - return nil -} - -func (o *WebSearchTool) GetType() string { - return "web_search" -} - -func (o *WebSearchTool) GetTrigger() *Trigger { - if o == nil { - return nil - } - return o.Trigger -} diff --git a/tests/mockserver/internal/sdk/models/operations/getv1search.go b/tests/mockserver/internal/sdk/models/operations/getv1search.go deleted file mode 100644 index 9e2170f..0000000 --- a/tests/mockserver/internal/sdk/models/operations/getv1search.go +++ /dev/null @@ -1,640 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package operations - -import ( - "errors" - "fmt" - "mockserver/internal/sdk/models/components" - "mockserver/internal/sdk/utils" - "time" -) - -type FreshnessType string - -const ( - FreshnessTypeFreshness FreshnessType = "Freshness" - FreshnessTypeStr FreshnessType = "str" -) - -// Freshness - Specifies the freshness of the results to return. -type Freshness struct { - Freshness *components.Freshness `queryParam:"inline"` - Str *string `queryParam:"inline"` - - Type FreshnessType -} - -func CreateFreshnessFreshness(freshness components.Freshness) Freshness { - typ := FreshnessTypeFreshness - - return Freshness{ - Freshness: &freshness, - Type: typ, - } -} - -func CreateFreshnessStr(str string) Freshness { - typ := FreshnessTypeStr - - return Freshness{ - Str: &str, - Type: typ, - } -} - -func (u *Freshness) UnmarshalJSON(data []byte) error { - - var freshness components.Freshness = components.Freshness("") - if err := utils.UnmarshalJSON(data, &freshness, "", true, nil); err == nil { - u.Freshness = &freshness - u.Type = FreshnessTypeFreshness - return nil - } - - var str string = "" - if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { - u.Str = &str - u.Type = FreshnessTypeStr - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for Freshness", string(data)) -} - -func (u Freshness) MarshalJSON() ([]byte, error) { - if u.Freshness != nil { - return utils.MarshalJSON(u.Freshness, "", true) - } - - if u.Str != nil { - return utils.MarshalJSON(u.Str, "", true) - } - - return nil, errors.New("could not marshal union type Freshness: all fields are null") -} - -type CountryType string - -const ( - CountryTypeCountry CountryType = "Country" - CountryTypeStr CountryType = "str" -) - -// Country - The country code that determines the geographical focus of the web results. -type Country struct { - Country *components.Country `queryParam:"inline"` - Str *string `queryParam:"inline"` - - Type CountryType -} - -func CreateCountryCountry(country components.Country) Country { - typ := CountryTypeCountry - - return Country{ - Country: &country, - Type: typ, - } -} - -func CreateCountryStr(str string) Country { - typ := CountryTypeStr - - return Country{ - Str: &str, - Type: typ, - } -} - -func (u *Country) UnmarshalJSON(data []byte) error { - - var country components.Country = components.Country("") - if err := utils.UnmarshalJSON(data, &country, "", true, nil); err == nil { - u.Country = &country - u.Type = CountryTypeCountry - return nil - } - - var str string = "" - if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { - u.Str = &str - u.Type = CountryTypeStr - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for Country", string(data)) -} - -func (u Country) MarshalJSON() ([]byte, error) { - if u.Country != nil { - return utils.MarshalJSON(u.Country, "", true) - } - - if u.Str != nil { - return utils.MarshalJSON(u.Str, "", true) - } - - return nil, errors.New("could not marshal union type Country: all fields are null") -} - -type SafesearchType string - -const ( - SafesearchTypeSafeSearch SafesearchType = "SafeSearch" - SafesearchTypeStr SafesearchType = "str" -) - -// Safesearch - Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. -type Safesearch struct { - SafeSearch *components.SafeSearch `queryParam:"inline"` - Str *string `queryParam:"inline"` - - Type SafesearchType -} - -func CreateSafesearchSafeSearch(safeSearch components.SafeSearch) Safesearch { - typ := SafesearchTypeSafeSearch - - return Safesearch{ - SafeSearch: &safeSearch, - Type: typ, - } -} - -func CreateSafesearchStr(str string) Safesearch { - typ := SafesearchTypeStr - - return Safesearch{ - Str: &str, - Type: typ, - } -} - -func (u *Safesearch) UnmarshalJSON(data []byte) error { - - var safeSearch components.SafeSearch = components.SafeSearch("") - if err := utils.UnmarshalJSON(data, &safeSearch, "", true, nil); err == nil { - u.SafeSearch = &safeSearch - u.Type = SafesearchTypeSafeSearch - return nil - } - - var str string = "" - if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { - u.Str = &str - u.Type = SafesearchTypeStr - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for Safesearch", string(data)) -} - -func (u Safesearch) MarshalJSON() ([]byte, error) { - if u.SafeSearch != nil { - return utils.MarshalJSON(u.SafeSearch, "", true) - } - - if u.Str != nil { - return utils.MarshalJSON(u.Str, "", true) - } - - return nil, errors.New("could not marshal union type Safesearch: all fields are null") -} - -type LivecrawlType string - -const ( - LivecrawlTypeLiveCrawl LivecrawlType = "LiveCrawl" - LivecrawlTypeStr LivecrawlType = "str" -) - -// Livecrawl - Indicates which section(s) of search results to livecrawl and return full page content. -type Livecrawl struct { - LiveCrawl *components.LiveCrawl `queryParam:"inline"` - Str *string `queryParam:"inline"` - - Type LivecrawlType -} - -func CreateLivecrawlLiveCrawl(liveCrawl components.LiveCrawl) Livecrawl { - typ := LivecrawlTypeLiveCrawl - - return Livecrawl{ - LiveCrawl: &liveCrawl, - Type: typ, - } -} - -func CreateLivecrawlStr(str string) Livecrawl { - typ := LivecrawlTypeStr - - return Livecrawl{ - Str: &str, - Type: typ, - } -} - -func (u *Livecrawl) UnmarshalJSON(data []byte) error { - - var liveCrawl components.LiveCrawl = components.LiveCrawl("") - if err := utils.UnmarshalJSON(data, &liveCrawl, "", true, nil); err == nil { - u.LiveCrawl = &liveCrawl - u.Type = LivecrawlTypeLiveCrawl - return nil - } - - var str string = "" - if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { - u.Str = &str - u.Type = LivecrawlTypeStr - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for Livecrawl", string(data)) -} - -func (u Livecrawl) MarshalJSON() ([]byte, error) { - if u.LiveCrawl != nil { - return utils.MarshalJSON(u.LiveCrawl, "", true) - } - - if u.Str != nil { - return utils.MarshalJSON(u.Str, "", true) - } - - return nil, errors.New("could not marshal union type Livecrawl: all fields are null") -} - -type LivecrawlFormatsType string - -const ( - LivecrawlFormatsTypeLiveCrawlFormats LivecrawlFormatsType = "LiveCrawlFormats" - LivecrawlFormatsTypeStr LivecrawlFormatsType = "str" -) - -// LivecrawlFormats - Indicates the format of the livecrawled content. -type LivecrawlFormats struct { - LiveCrawlFormats *components.LiveCrawlFormats `queryParam:"inline"` - Str *string `queryParam:"inline"` - - Type LivecrawlFormatsType -} - -func CreateLivecrawlFormatsLiveCrawlFormats(liveCrawlFormats components.LiveCrawlFormats) LivecrawlFormats { - typ := LivecrawlFormatsTypeLiveCrawlFormats - - return LivecrawlFormats{ - LiveCrawlFormats: &liveCrawlFormats, - Type: typ, - } -} - -func CreateLivecrawlFormatsStr(str string) LivecrawlFormats { - typ := LivecrawlFormatsTypeStr - - return LivecrawlFormats{ - Str: &str, - Type: typ, - } -} - -func (u *LivecrawlFormats) UnmarshalJSON(data []byte) error { - - var liveCrawlFormats components.LiveCrawlFormats = components.LiveCrawlFormats("") - if err := utils.UnmarshalJSON(data, &liveCrawlFormats, "", true, nil); err == nil { - u.LiveCrawlFormats = &liveCrawlFormats - u.Type = LivecrawlFormatsTypeLiveCrawlFormats - return nil - } - - var str string = "" - if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { - u.Str = &str - u.Type = LivecrawlFormatsTypeStr - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for LivecrawlFormats", string(data)) -} - -func (u LivecrawlFormats) MarshalJSON() ([]byte, error) { - if u.LiveCrawlFormats != nil { - return utils.MarshalJSON(u.LiveCrawlFormats, "", true) - } - - if u.Str != nil { - return utils.MarshalJSON(u.Str, "", true) - } - - return nil, errors.New("could not marshal union type LivecrawlFormats: all fields are null") -} - -type GetV1SearchRequest struct { - // The search query used to retrieve relevant results from the web. You can also include [search operators](#search-operators) to refine your search. - Query string `default:"Your query" queryParam:"style=form,explode=true,name=query"` - // Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). - Count *int64 `queryParam:"style=form,explode=true,name=count"` - // Specifies the freshness of the results to return. - Freshness *Freshness `queryParam:"style=form,explode=true,name=freshness"` - // 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`. - Offset *int64 `queryParam:"style=form,explode=true,name=offset"` - // The country code that determines the geographical focus of the web results. - Country *Country `queryParam:"style=form,explode=true,name=country"` - // Configures the safesearch filter for content moderation. This allows you to decide whether to return NSFW content or not. - Safesearch *Safesearch `queryParam:"style=form,explode=true,name=safesearch"` - // Indicates which section(s) of search results to livecrawl and return full page content. - Livecrawl *Livecrawl `queryParam:"style=form,explode=true,name=livecrawl"` - // Indicates the format of the livecrawled content. - LivecrawlFormats *LivecrawlFormats `queryParam:"style=form,explode=true,name=livecrawl_formats"` -} - -func (g GetV1SearchRequest) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(g, "", false) -} - -func (g *GetV1SearchRequest) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &g, "", false, []string{"query"}); err != nil { - return err - } - return nil -} - -func (o *GetV1SearchRequest) GetQuery() string { - if o == nil { - return "" - } - return o.Query -} - -func (o *GetV1SearchRequest) GetCount() *int64 { - if o == nil { - return nil - } - return o.Count -} - -func (o *GetV1SearchRequest) GetFreshness() *Freshness { - if o == nil { - return nil - } - return o.Freshness -} - -func (o *GetV1SearchRequest) GetOffset() *int64 { - if o == nil { - return nil - } - return o.Offset -} - -func (o *GetV1SearchRequest) GetCountry() *Country { - if o == nil { - return nil - } - return o.Country -} - -func (o *GetV1SearchRequest) GetSafesearch() *Safesearch { - if o == nil { - return nil - } - return o.Safesearch -} - -func (o *GetV1SearchRequest) GetLivecrawl() *Livecrawl { - if o == nil { - return nil - } - return o.Livecrawl -} - -func (o *GetV1SearchRequest) GetLivecrawlFormats() *LivecrawlFormats { - if o == nil { - return nil - } - return o.LivecrawlFormats -} - -type Web struct { - // The URL of the specific search result. - URL *string `json:"url,omitempty"` - // The title or name of the search result. - Title *string `json:"title,omitempty"` - // A brief description of the content of the search result. - Description *string `json:"description,omitempty"` - // An array of text snippets from the search result, providing a preview of the content. - Snippets []string `json:"snippets,omitempty"` - // URL of the thumbnail. - ThumbnailURL *string `json:"thumbnail_url,omitempty"` - // The age of the search result. - PageAge *time.Time `json:"page_age,omitempty"` - // The URL of the favicon of the search result's domain. - FaviconURL *string `json:"favicon_url,omitempty"` -} - -func (w Web) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(w, "", false) -} - -func (w *Web) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &w, "", false, nil); err != nil { - return err - } - return nil -} - -func (o *Web) GetURL() *string { - if o == nil { - return nil - } - return o.URL -} - -func (o *Web) GetTitle() *string { - if o == nil { - return nil - } - return o.Title -} - -func (o *Web) GetDescription() *string { - if o == nil { - return nil - } - return o.Description -} - -func (o *Web) GetSnippets() []string { - if o == nil { - return nil - } - return o.Snippets -} - -func (o *Web) GetThumbnailURL() *string { - if o == nil { - return nil - } - return o.ThumbnailURL -} - -func (o *Web) GetPageAge() *time.Time { - if o == nil { - return nil - } - return o.PageAge -} - -func (o *Web) GetFaviconURL() *string { - if o == nil { - return nil - } - return o.FaviconURL -} - -type News struct { - // The title of the news result. - Title *string `json:"title,omitempty"` - // A brief description of the content of the news result. - Description *string `json:"description,omitempty"` - // UTC timestamp of the article's publication date. - PageAge *time.Time `json:"page_age,omitempty"` - // URL of the thumbnail. - ThumbnailURL *string `json:"thumbnail_url,omitempty"` - // The URL of the news result. - URL *string `json:"url,omitempty"` -} - -func (n News) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(n, "", false) -} - -func (n *News) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &n, "", false, nil); err != nil { - return err - } - return nil -} - -func (o *News) GetTitle() *string { - if o == nil { - return nil - } - return o.Title -} - -func (o *News) GetDescription() *string { - if o == nil { - return nil - } - return o.Description -} - -func (o *News) GetPageAge() *time.Time { - if o == nil { - return nil - } - return o.PageAge -} - -func (o *News) GetThumbnailURL() *string { - if o == nil { - return nil - } - return o.ThumbnailURL -} - -func (o *News) GetURL() *string { - if o == nil { - return nil - } - return o.URL -} - -type Results struct { - Web []Web `json:"web,omitempty"` - News []News `json:"news,omitempty"` -} - -func (o *Results) GetWeb() []Web { - if o == nil { - return nil - } - return o.Web -} - -func (o *Results) GetNews() []News { - if o == nil { - return nil - } - return o.News -} - -type Metadata struct { - SearchUUID *string `json:"search_uuid,omitempty"` - // Returns the search query used to retrieve the results. - Query *string `json:"query,omitempty"` - Latency *float64 `json:"latency,omitempty"` -} - -func (o *Metadata) GetSearchUUID() *string { - if o == nil { - return nil - } - return o.SearchUUID -} - -func (o *Metadata) GetQuery() *string { - if o == nil { - return nil - } - return o.Query -} - -func (o *Metadata) GetLatency() *float64 { - if o == nil { - return nil - } - return o.Latency -} - -// GetV1SearchResponseBody - A JSON object containing unified search results from web and news sources -type GetV1SearchResponseBody struct { - Results *Results `json:"results,omitempty"` - Metadata *Metadata `json:"metadata,omitempty"` -} - -func (o *GetV1SearchResponseBody) GetResults() *Results { - if o == nil { - return nil - } - return o.Results -} - -func (o *GetV1SearchResponseBody) GetMetadata() *Metadata { - if o == nil { - return nil - } - return o.Metadata -} - -type GetV1SearchResponse struct { - HTTPMeta components.HTTPMetadata `json:"-"` - // A JSON object containing unified search results from web and news sources - Object *GetV1SearchResponseBody -} - -func (o *GetV1SearchResponse) GetHTTPMeta() components.HTTPMetadata { - if o == nil { - return components.HTTPMetadata{} - } - return o.HTTPMeta -} - -func (o *GetV1SearchResponse) GetObject() *GetV1SearchResponseBody { - if o == nil { - return nil - } - return o.Object -} diff --git a/tests/mockserver/internal/sdk/models/operations/postv1agentsruns.go b/tests/mockserver/internal/sdk/models/operations/postv1agentsruns.go deleted file mode 100644 index 903ceb2..0000000 --- a/tests/mockserver/internal/sdk/models/operations/postv1agentsruns.go +++ /dev/null @@ -1,591 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package operations - -import ( - "errors" - "fmt" - "mockserver/internal/sdk/models/components" - "mockserver/internal/sdk/types/stream" - "mockserver/internal/sdk/utils" -) - -type AgentType string - -const ( - AgentTypeAgentType AgentType = "AgentType" - AgentTypeStr AgentType = "str" -) - -// Agent type (express, advanced) or custom agent UUID -type Agent struct { - AgentType *components.AgentType `queryParam:"inline"` - Str *string `queryParam:"inline"` - - Type AgentType -} - -func CreateAgentAgentType(agentType components.AgentType) Agent { - typ := AgentTypeAgentType - - return Agent{ - AgentType: &agentType, - Type: typ, - } -} - -func CreateAgentStr(str string) Agent { - typ := AgentTypeStr - - return Agent{ - Str: &str, - Type: typ, - } -} - -func (u *Agent) UnmarshalJSON(data []byte) error { - - var agentType components.AgentType = components.AgentType("") - if err := utils.UnmarshalJSON(data, &agentType, "", true, nil); err == nil { - u.AgentType = &agentType - u.Type = AgentTypeAgentType - return nil - } - - var str string = "" - if err := utils.UnmarshalJSON(data, &str, "", true, nil); err == nil { - u.Str = &str - u.Type = AgentTypeStr - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for Agent", string(data)) -} - -func (u Agent) MarshalJSON() ([]byte, error) { - if u.AgentType != nil { - return utils.MarshalJSON(u.AgentType, "", true) - } - - if u.Str != nil { - return utils.MarshalJSON(u.Str, "", true) - } - - return nil, errors.New("could not marshal union type Agent: all fields are null") -} - -type WorkflowConfig struct { - // Maximum number of workflow steps - MaxWorkflowSteps *int64 `default:"5" json:"max_workflow_steps"` -} - -func (w WorkflowConfig) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(w, "", false) -} - -func (w *WorkflowConfig) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &w, "", false, nil); err != nil { - return err - } - return nil -} - -func (o *WorkflowConfig) GetMaxWorkflowSteps() *int64 { - if o == nil { - return nil - } - return o.MaxWorkflowSteps -} - -type PostV1AgentsRunsRequest struct { - // Agent type (express, advanced) or custom agent UUID - Agent Agent `json:"agent"` - // User input prompt. - Input string `json:"input"` - Stream *bool `default:"false" json:"stream"` - // Array of tool configurations - Tools []components.Tool `json:"tools,omitempty"` - // Response verbosity level - Verbosity *components.Verbosity `json:"verbosity,omitempty"` - WorkflowConfig *WorkflowConfig `json:"workflow_config,omitempty"` -} - -func (p PostV1AgentsRunsRequest) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(p, "", false) -} - -func (p *PostV1AgentsRunsRequest) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &p, "", false, []string{"agent", "input"}); err != nil { - return err - } - return nil -} - -func (o *PostV1AgentsRunsRequest) GetAgent() Agent { - if o == nil { - return Agent{} - } - return o.Agent -} - -func (o *PostV1AgentsRunsRequest) GetInput() string { - if o == nil { - return "" - } - return o.Input -} - -func (o *PostV1AgentsRunsRequest) GetStream() *bool { - if o == nil { - return nil - } - return o.Stream -} - -func (o *PostV1AgentsRunsRequest) GetTools() []components.Tool { - if o == nil { - return nil - } - return o.Tools -} - -func (o *PostV1AgentsRunsRequest) GetVerbosity() *components.Verbosity { - if o == nil { - return nil - } - return o.Verbosity -} - -func (o *PostV1AgentsRunsRequest) GetWorkflowConfig() *WorkflowConfig { - if o == nil { - return nil - } - return o.WorkflowConfig -} - -type ForbiddenError struct { - Status string `json:"status"` - Code string `json:"code"` - Title string `json:"title"` - Detail string `json:"detail"` -} - -func (o *ForbiddenError) GetStatus() string { - if o == nil { - return "" - } - return o.Status -} - -func (o *ForbiddenError) GetCode() string { - if o == nil { - return "" - } - return o.Code -} - -func (o *ForbiddenError) GetTitle() string { - if o == nil { - return "" - } - return o.Title -} - -func (o *ForbiddenError) GetDetail() string { - if o == nil { - return "" - } - return o.Detail -} - -type UnauthorizedError struct { - Status string `json:"status"` - Code string `json:"code"` - Title string `json:"title"` - Detail string `json:"detail"` -} - -func (o *UnauthorizedError) GetStatus() string { - if o == nil { - return "" - } - return o.Status -} - -func (o *UnauthorizedError) GetCode() string { - if o == nil { - return "" - } - return o.Code -} - -func (o *UnauthorizedError) GetTitle() string { - if o == nil { - return "" - } - return o.Title -} - -func (o *UnauthorizedError) GetDetail() string { - if o == nil { - return "" - } - return o.Detail -} - -type BadRequestError struct { - Status string `json:"status"` - Code string `json:"code"` - Title string `json:"title"` - Detail string `json:"detail"` -} - -func (o *BadRequestError) GetStatus() string { - if o == nil { - return "" - } - return o.Status -} - -func (o *BadRequestError) GetCode() string { - if o == nil { - return "" - } - return o.Code -} - -func (o *BadRequestError) GetTitle() string { - if o == nil { - return "" - } - return o.Title -} - -func (o *BadRequestError) GetDetail() string { - if o == nil { - return "" - } - return o.Detail -} - -type Response struct { - // The type of the response. - Type *string `json:"type,omitempty"` - // The index of the output in the response. - OutputIndex *int64 `json:"output_index,omitempty"` - // The delta of the response. - Delta *string `json:"delta,omitempty"` - Full *components.FullResponse `json:"full,omitempty"` -} - -func (o *Response) GetType() *string { - if o == nil { - return nil - } - return o.Type -} - -func (o *Response) GetOutputIndex() *int64 { - if o == nil { - return nil - } - return o.OutputIndex -} - -func (o *Response) GetDelta() *string { - if o == nil { - return nil - } - return o.Delta -} - -func (o *Response) GetFull() *components.FullResponse { - if o == nil { - return nil - } - return o.Full -} - -type Data struct { - // Sequence number of the SSE event, starts from 0. Same as `id` field. - SeqID *int64 `json:"seq_id,omitempty"` - // The type of the SSE event. Same as `event` field. - Type *string `json:"type,omitempty"` - Response *Response `json:"response,omitempty"` -} - -func (o *Data) GetSeqID() *int64 { - if o == nil { - return nil - } - return o.SeqID -} - -func (o *Data) GetType() *string { - if o == nil { - return nil - } - return o.Type -} - -func (o *Data) GetResponse() *Response { - if o == nil { - return nil - } - return o.Response -} - -// PostV1AgentsRunsEventStreamResponseBody - Inference response in application/json or text/event-stream format. -type PostV1AgentsRunsEventStreamResponseBody struct { - // Sequence number of the SSE event - ID *string `json:"id,omitempty"` - // The type of the SSE event. - Event *string `json:"event,omitempty"` - Data *Data `json:"data,omitempty"` -} - -func (o *PostV1AgentsRunsEventStreamResponseBody) GetID() *string { - if o == nil { - return nil - } - return o.ID -} - -func (o *PostV1AgentsRunsEventStreamResponseBody) GetEvent() *string { - if o == nil { - return nil - } - return o.Event -} - -func (o *PostV1AgentsRunsEventStreamResponseBody) GetData() *Data { - if o == nil { - return nil - } - return o.Data -} - -func (o PostV1AgentsRunsEventStreamResponseBody) GetEventEncoding(event string) (string, error) { - return "application/json", nil -} - -type Content struct { -} - -func (c Content) MarshalJSON() ([]byte, error) { - return utils.MarshalJSON(c, "", false) -} - -func (c *Content) UnmarshalJSON(data []byte) error { - if err := utils.UnmarshalJSON(data, &c, "", false, nil); err != nil { - return err - } - return nil -} - -type ContentUnion1Type string - -const ( - ContentUnion1TypeArrayOfAny ContentUnion1Type = "arrayOfAny" - ContentUnion1TypeContent ContentUnion1Type = "content" -) - -type ContentUnion1 struct { - ArrayOfAny []any `queryParam:"inline"` - Content *Content `queryParam:"inline"` - - Type ContentUnion1Type -} - -func CreateContentUnion1ArrayOfAny(arrayOfAny []any) ContentUnion1 { - typ := ContentUnion1TypeArrayOfAny - - return ContentUnion1{ - ArrayOfAny: arrayOfAny, - Type: typ, - } -} - -func CreateContentUnion1Content(content Content) ContentUnion1 { - typ := ContentUnion1TypeContent - - return ContentUnion1{ - Content: &content, - Type: typ, - } -} - -func (u *ContentUnion1) UnmarshalJSON(data []byte) error { - - var arrayOfAny []any = []any{} - if err := utils.UnmarshalJSON(data, &arrayOfAny, "", true, nil); err == nil { - u.ArrayOfAny = arrayOfAny - u.Type = ContentUnion1TypeArrayOfAny - return nil - } - - var content Content = Content{} - if err := utils.UnmarshalJSON(data, &content, "", true, nil); err == nil { - u.Content = &content - u.Type = ContentUnion1TypeContent - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for ContentUnion1", string(data)) -} - -func (u ContentUnion1) MarshalJSON() ([]byte, error) { - if u.ArrayOfAny != nil { - return utils.MarshalJSON(u.ArrayOfAny, "", true) - } - - if u.Content != nil { - return utils.MarshalJSON(u.Content, "", true) - } - - return nil, errors.New("could not marshal union type ContentUnion1: all fields are null") -} - -type ContentUnion2Type string - -const ( - ContentUnion2TypeMapOfAny ContentUnion2Type = "mapOfAny" - ContentUnion2TypeContentUnion1 ContentUnion2Type = "content_union_1" -) - -// ContentUnion2 - Returns the exact search query or list of sources. -type ContentUnion2 struct { - MapOfAny map[string]any `queryParam:"inline"` - ContentUnion1 *ContentUnion1 `queryParam:"inline"` - - Type ContentUnion2Type -} - -func CreateContentUnion2MapOfAny(mapOfAny map[string]any) ContentUnion2 { - typ := ContentUnion2TypeMapOfAny - - return ContentUnion2{ - MapOfAny: mapOfAny, - Type: typ, - } -} - -func CreateContentUnion2ContentUnion1(contentUnion1 ContentUnion1) ContentUnion2 { - typ := ContentUnion2TypeContentUnion1 - - return ContentUnion2{ - ContentUnion1: &contentUnion1, - Type: typ, - } -} - -func (u *ContentUnion2) UnmarshalJSON(data []byte) error { - - var mapOfAny map[string]any = map[string]any{} - if err := utils.UnmarshalJSON(data, &mapOfAny, "", true, nil); err == nil { - u.MapOfAny = mapOfAny - u.Type = ContentUnion2TypeMapOfAny - return nil - } - - var contentUnion1 ContentUnion1 = ContentUnion1{} - if err := utils.UnmarshalJSON(data, &contentUnion1, "", true, nil); err == nil { - u.ContentUnion1 = &contentUnion1 - u.Type = ContentUnion2TypeContentUnion1 - return nil - } - - return fmt.Errorf("could not unmarshal `%s` into any supported union types for ContentUnion2", string(data)) -} - -func (u ContentUnion2) MarshalJSON() ([]byte, error) { - if u.MapOfAny != nil { - return utils.MarshalJSON(u.MapOfAny, "", true) - } - - if u.ContentUnion1 != nil { - return utils.MarshalJSON(u.ContentUnion1, "", true) - } - - return nil, errors.New("could not marshal union type ContentUnion2: all fields are null") -} - -type Output struct { - // The type of the output. - Type *string `json:"type,omitempty"` - // The text of the output. - Text *string `json:"text,omitempty"` - // Returns the exact search query or list of sources. - Content *ContentUnion2 `json:"content,omitempty"` - // The agent used to generate the response. - Agent *string `json:"agent,omitempty"` -} - -func (o *Output) GetType() *string { - if o == nil { - return nil - } - return o.Type -} - -func (o *Output) GetText() *string { - if o == nil { - return nil - } - return o.Text -} - -func (o *Output) GetContent() *ContentUnion2 { - if o == nil { - return nil - } - return o.Content -} - -func (o *Output) GetAgent() *string { - if o == nil { - return nil - } - return o.Agent -} - -// PostV1AgentsRunsResponseBody - Inference response in application/json or text/event-stream format. -type PostV1AgentsRunsResponseBody struct { - Output []Output `json:"output,omitempty"` -} - -func (o *PostV1AgentsRunsResponseBody) GetOutput() []Output { - if o == nil { - return nil - } - return o.Output -} - -type PostV1AgentsRunsResponse struct { - HTTPMeta components.HTTPMetadata `json:"-"` - // Inference response in application/json or text/event-stream format. - TwoHundredApplicationJSONObject *PostV1AgentsRunsResponseBody - // Inference response in application/json or text/event-stream format. - TwoHundredTextEventStreamObject *stream.EventStream[PostV1AgentsRunsEventStreamResponseBody] -} - -func (o *PostV1AgentsRunsResponse) GetHTTPMeta() components.HTTPMetadata { - if o == nil { - return components.HTTPMetadata{} - } - return o.HTTPMeta -} - -func (o *PostV1AgentsRunsResponse) GetTwoHundredApplicationJSONObject() *PostV1AgentsRunsResponseBody { - if o == nil { - return nil - } - return o.TwoHundredApplicationJSONObject -} - -func (o *PostV1AgentsRunsResponse) GetTwoHundredTextEventStreamObject() *stream.EventStream[PostV1AgentsRunsEventStreamResponseBody] { - if o == nil { - return nil - } - return o.TwoHundredTextEventStreamObject -} diff --git a/tests/mockserver/internal/sdk/models/operations/postv1contents.go b/tests/mockserver/internal/sdk/models/operations/postv1contents.go index 8fbefda..622a2eb 100644 --- a/tests/mockserver/internal/sdk/models/operations/postv1contents.go +++ b/tests/mockserver/internal/sdk/models/operations/postv1contents.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package operations diff --git a/tests/mockserver/internal/sdk/models/sdkerrors/getv1search.go b/tests/mockserver/internal/sdk/models/sdkerrors/getv1search.go deleted file mode 100644 index 7e84e7b..0000000 --- a/tests/mockserver/internal/sdk/models/sdkerrors/getv1search.go +++ /dev/null @@ -1,48 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package sdkerrors - -import ( - "encoding/json" - "mockserver/internal/sdk/models/components" -) - -// GetV1SearchInternalServerError - Internal Server Error during authentication/authorization middleware. -type GetV1SearchInternalServerError struct { - Detail *string `json:"detail,omitempty"` - HTTPMeta components.HTTPMetadata `json:"-"` -} - -var _ error = &GetV1SearchInternalServerError{} - -func (e *GetV1SearchInternalServerError) Error() string { - data, _ := json.Marshal(e) - return string(data) -} - -// GetV1SearchForbiddenError - Forbidden. API key lacks scope for this path. -type GetV1SearchForbiddenError struct { - Detail *string `json:"detail,omitempty"` - HTTPMeta components.HTTPMetadata `json:"-"` -} - -var _ error = &GetV1SearchForbiddenError{} - -func (e *GetV1SearchForbiddenError) Error() string { - data, _ := json.Marshal(e) - return string(data) -} - -// GetV1SearchUnauthorizedError - Unauthorized. Problems with API key. -type GetV1SearchUnauthorizedError struct { - // Error detail message. - Detail *string `json:"detail,omitempty"` - HTTPMeta components.HTTPMetadata `json:"-"` -} - -var _ error = &GetV1SearchUnauthorizedError{} - -func (e *GetV1SearchUnauthorizedError) Error() string { - data, _ := json.Marshal(e) - return string(data) -} diff --git a/tests/mockserver/internal/sdk/models/sdkerrors/postv1agentsruns.go b/tests/mockserver/internal/sdk/models/sdkerrors/postv1agentsruns.go deleted file mode 100644 index 1b8321e..0000000 --- a/tests/mockserver/internal/sdk/models/sdkerrors/postv1agentsruns.go +++ /dev/null @@ -1,48 +0,0 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - -package sdkerrors - -import ( - "encoding/json" - "mockserver/internal/sdk/models/components" - "mockserver/internal/sdk/models/operations" -) - -// PostV1AgentsRunsForbiddenError - Forbidden. API key lacks scope for this path. -type PostV1AgentsRunsForbiddenError struct { - Errors []operations.ForbiddenError `json:"errors"` - HTTPMeta components.HTTPMetadata `json:"-"` -} - -var _ error = &PostV1AgentsRunsForbiddenError{} - -func (e *PostV1AgentsRunsForbiddenError) Error() string { - data, _ := json.Marshal(e) - return string(data) -} - -// PostV1AgentsRunsUnauthorizedError - Unauthorized. Problems with API key. -type PostV1AgentsRunsUnauthorizedError struct { - Errors []operations.UnauthorizedError `json:"errors"` - HTTPMeta components.HTTPMetadata `json:"-"` -} - -var _ error = &PostV1AgentsRunsUnauthorizedError{} - -func (e *PostV1AgentsRunsUnauthorizedError) Error() string { - data, _ := json.Marshal(e) - return string(data) -} - -// BadRequestError - Bad Request. Invalid or malformed request body/parameters. -type BadRequestError struct { - Errors []operations.BadRequestError `json:"errors"` - HTTPMeta components.HTTPMetadata `json:"-"` -} - -var _ error = &BadRequestError{} - -func (e *BadRequestError) Error() string { - data, _ := json.Marshal(e) - return string(data) -} diff --git a/tests/mockserver/internal/sdk/models/sdkerrors/postv1contents.go b/tests/mockserver/internal/sdk/models/sdkerrors/postv1contents.go index 62938fb..6a49d47 100644 --- a/tests/mockserver/internal/sdk/models/sdkerrors/postv1contents.go +++ b/tests/mockserver/internal/sdk/models/sdkerrors/postv1contents.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package sdkerrors diff --git a/tests/mockserver/internal/sdk/types/bigint.go b/tests/mockserver/internal/sdk/types/bigint.go index 9c6a086..8fae84d 100644 --- a/tests/mockserver/internal/sdk/types/bigint.go +++ b/tests/mockserver/internal/sdk/types/bigint.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package types diff --git a/tests/mockserver/internal/sdk/types/date.go b/tests/mockserver/internal/sdk/types/date.go index 5b2782f..18188bb 100644 --- a/tests/mockserver/internal/sdk/types/date.go +++ b/tests/mockserver/internal/sdk/types/date.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package types diff --git a/tests/mockserver/internal/sdk/types/datetime.go b/tests/mockserver/internal/sdk/types/datetime.go index 3eff332..112dede 100644 --- a/tests/mockserver/internal/sdk/types/datetime.go +++ b/tests/mockserver/internal/sdk/types/datetime.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package types diff --git a/tests/mockserver/internal/sdk/types/decimal.go b/tests/mockserver/internal/sdk/types/decimal.go index d8429bc..269b1ca 100644 --- a/tests/mockserver/internal/sdk/types/decimal.go +++ b/tests/mockserver/internal/sdk/types/decimal.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package types diff --git a/tests/mockserver/internal/sdk/types/jsonl/jsonl.go b/tests/mockserver/internal/sdk/types/jsonl/jsonl.go index 26df95c..01557a8 100644 --- a/tests/mockserver/internal/sdk/types/jsonl/jsonl.go +++ b/tests/mockserver/internal/sdk/types/jsonl/jsonl.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package jsonl diff --git a/tests/mockserver/internal/sdk/types/pointers.go b/tests/mockserver/internal/sdk/types/pointers.go index 35c439d..479bc2b 100644 --- a/tests/mockserver/internal/sdk/types/pointers.go +++ b/tests/mockserver/internal/sdk/types/pointers.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package types diff --git a/tests/mockserver/internal/sdk/types/stream/stream.go b/tests/mockserver/internal/sdk/types/stream/stream.go index 365d8cf..1562f71 100644 --- a/tests/mockserver/internal/sdk/types/stream/stream.go +++ b/tests/mockserver/internal/sdk/types/stream/stream.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package stream diff --git a/tests/mockserver/internal/sdk/utils/json.go b/tests/mockserver/internal/sdk/utils/json.go index 764f645..ad37261 100644 --- a/tests/mockserver/internal/sdk/utils/json.go +++ b/tests/mockserver/internal/sdk/utils/json.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package utils diff --git a/tests/mockserver/internal/sdk/utils/reflect.go b/tests/mockserver/internal/sdk/utils/reflect.go index 255f3dd..029e9b1 100644 --- a/tests/mockserver/internal/sdk/utils/reflect.go +++ b/tests/mockserver/internal/sdk/utils/reflect.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package utils diff --git a/tests/mockserver/internal/sdk/utils/sort.go b/tests/mockserver/internal/sdk/utils/sort.go index 6b265be..0e22a98 100644 --- a/tests/mockserver/internal/sdk/utils/sort.go +++ b/tests/mockserver/internal/sdk/utils/sort.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package utils diff --git a/tests/mockserver/internal/server/doc.go b/tests/mockserver/internal/server/doc.go index 666dc10..499d860 100644 --- a/tests/mockserver/internal/server/doc.go +++ b/tests/mockserver/internal/server/doc.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. // Package server implements the HTTP server. package server diff --git a/tests/mockserver/internal/server/generated_handlers.go b/tests/mockserver/internal/server/generated_handlers.go index 1ea1635..6947fc9 100644 --- a/tests/mockserver/internal/server/generated_handlers.go +++ b/tests/mockserver/internal/server/generated_handlers.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package server diff --git a/tests/mockserver/internal/server/internal_handlers.go b/tests/mockserver/internal/server/internal_handlers.go index 1c24861..a58586c 100644 --- a/tests/mockserver/internal/server/internal_handlers.go +++ b/tests/mockserver/internal/server/internal_handlers.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package server diff --git a/tests/mockserver/internal/server/server.go b/tests/mockserver/internal/server/server.go index 1223961..b54bd55 100644 --- a/tests/mockserver/internal/server/server.go +++ b/tests/mockserver/internal/server/server.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package server diff --git a/tests/mockserver/internal/server/server_option.go b/tests/mockserver/internal/server/server_option.go index ff85a67..aac6682 100644 --- a/tests/mockserver/internal/server/server_option.go +++ b/tests/mockserver/internal/server/server_option.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package server diff --git a/tests/mockserver/internal/tracking/requesttracker.go b/tests/mockserver/internal/tracking/requesttracker.go index 1d9131d..96ccd29 100644 --- a/tests/mockserver/internal/tracking/requesttracker.go +++ b/tests/mockserver/internal/tracking/requesttracker.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package tracking diff --git a/tests/mockserver/main.go b/tests/mockserver/main.go index 2270443..2f462a9 100644 --- a/tests/mockserver/main.go +++ b/tests/mockserver/main.go @@ -1,4 +1,3 @@ -// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. package main diff --git a/tests/test_answer.py b/tests/test_answer.py new file mode 100644 index 0000000..fdcc863 --- /dev/null +++ b/tests/test_answer.py @@ -0,0 +1,282 @@ +"""Tests for youdotcom.answer — POST /v1/answer.""" + +import json + +import httpx +import pytest + +from youdotcom import You +from youdotcom.errors import ( + ForbiddenResponseError, + InternalServerErrorResponse, + PaymentRequiredResponseError, + UnauthorizedResponseError, + UnprocessableEntityResponseError, + YouDefaultError, +) +from youdotcom.models import AnswerResponse + +_ANSWER_BODY = json.dumps( + { + "answer": "Quantum computing advanced in 2025[[1, 2]].", + "citations": [ + {"source": "https://example.com/quantum", "excerpts": ["IBM announced a new processor."]}, + {"source": "https://example.com/ibm", "excerpts": ["Google achieved error correction.", "IBM unveiled 1000 qubits."]}, + ], + "results": { + "web": [ + {"url": "https://example.com/quantum", "title": "Quantum News", "snippets": ["IBM announced a new processor."], "page_age": "2025-06-25T11:41:00"}, + {"url": "https://example.com/ibm", "title": "IBM Quantum", "snippets": ["Google achieved error correction."]}, + ] + }, + } +) + + +def _make_handler(status: int = 200, body: str = _ANSWER_BODY): + def handler(request): + return httpx.Response( + status, headers={"content-type": "application/json"}, content=body + ) + + return handler + + +def _sync_you(handler, *, api_key: str | None = "test-key"): + kwargs: dict = { + "server_url": "http://mock.local", + "client": httpx.Client(transport=httpx.MockTransport(handler)), + } + if api_key is not None: + kwargs["api_key_auth"] = api_key + return You(**kwargs) + + +def _async_you(handler, *, api_key: str | None = "test-key"): + kwargs: dict = { + "server_url": "http://mock.local", + "async_client": httpx.AsyncClient(transport=httpx.MockTransport(handler)), + } + if api_key is not None: + kwargs["api_key_auth"] = api_key + return You(**kwargs) + + +class TestAnswerSuccess: + def test_returns_answer_response(self): + res = _sync_you(_make_handler(200)).answer(query="quantum computing 2025") + assert isinstance(res, AnswerResponse) + assert "Quantum computing" in res.answer + assert len(res.citations) == 2 + assert res.citations[0].source == "https://example.com/quantum" + assert len(res.citations[0].excerpts) == 1 + assert res.citations[1].source == "https://example.com/ibm" + assert len(res.citations[1].excerpts) == 2 + assert len(res.results.web) == 2 + assert res.results.web[0].title == "Quantum News" + assert res.results.web[0].page_age == "2025-06-25T11:41:00" + assert res.results.web[1].page_age is None + + def test_posts_to_answer_endpoint(self): + captured: dict = {} + + def handler(request): + captured["url"] = str(request.url) + captured["method"] = request.method + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_ANSWER_BODY + ) + + _sync_you(handler).answer(query="test") + assert captured["method"] == "POST" + assert "/v1/answer" in captured["url"] + + def test_domain_params_serialized(self): + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_ANSWER_BODY + ) + + _sync_you(handler).answer( + query="test", + include_domains=["nature.com", "science.org"], + country="US", + language="EN", + freshness="week", + ) + assert captured["body"]["include_domains"] == ["nature.com", "science.org"] + assert captured["body"]["country"] == "US" + assert captured["body"]["freshness"] == "week" + + def test_exclude_and_boost_domains_serialized(self): + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_ANSWER_BODY + ) + + _sync_you(handler).answer( + query="test", + exclude_domains=["spam.com"], + boost_domains=["reuters.com"], + ) + assert captured["body"]["exclude_domains"] == ["spam.com"] + assert captured["body"]["boost_domains"] == ["reuters.com"] + assert "include_domains" not in captured["body"] + + def test_server_url_override_honored(self): + """You(server_url=...) should be respected by answer.create().""" + captured: dict = {} + + def handler(request): + captured["url"] = str(request.url) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_ANSWER_BODY + ) + + you = You( + server_url="http://custom.local", + client=httpx.Client(transport=httpx.MockTransport(handler)), + api_key_auth="test-key", + ) + you.answer(query="test") + assert "http://custom.local" in captured["url"] + assert "/v1/answer" in captured["url"] + + def test_lowercase_language_and_country_normalized(self): + """language='en' and country='us' should be normalized to uppercase.""" + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_ANSWER_BODY + ) + + _sync_you(handler).answer(query="test", language="en", country="us") + assert captured["body"]["language"] == "EN" + assert captured["body"]["country"] == "US" + + def test_omits_optional_params_when_not_set(self): + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_ANSWER_BODY + ) + + _sync_you(handler).answer(query="test") + body = captured["body"] + assert "freshness" not in body + assert "country" not in body + assert "include_domains" not in body + assert body["query"] == "test" + + @pytest.mark.asyncio + async def test_async_returns_answer_response(self): + res = await _async_you(_make_handler(200)).answer_async(query="quantum") + assert isinstance(res, AnswerResponse) + assert len(res.citations) == 2 + assert len(res.results.web) == 2 + + +class TestAnswerErrors: + def test_402_raises_payment_required_error(self): + body = json.dumps({ + "error": "payment_required", + "message": "Insufficient credits", + "upgrade_url": "https://you.com/platform", + }) + with pytest.raises(PaymentRequiredResponseError) as exc_info: + _sync_you(_make_handler(402, body)).answer(query="test") + assert exc_info.value.status_code == 402 + assert exc_info.value.data.message == "Insufficient credits" + assert exc_info.value.data.upgrade_url == "https://you.com/platform" + + def test_402_with_usage_fields(self): + """402 response with optional limit/used/period/reset_at fields.""" + body = json.dumps({ + "error": "payment_required", + "message": "Daily limit exceeded", + "upgrade_url": "https://you.com/platform", + "limit": 100, + "used": 100, + "period": "day", + "reset_at": "2026-08-05T00:00:00Z", + }) + with pytest.raises(PaymentRequiredResponseError) as exc_info: + _sync_you(_make_handler(402, body)).answer(query="test") + assert exc_info.value.data.limit == 100 + assert exc_info.value.data.used == 100 + assert exc_info.value.data.period == "day" + assert exc_info.value.data.reset_at == "2026-08-05T00:00:00Z" + + def test_401_raises_unauthorized_error(self): + body = json.dumps({"detail": "Invalid or expired API key"}) + with pytest.raises(UnauthorizedResponseError): + _sync_you(_make_handler(401, body), api_key="bad-key").answer(query="test") + + def test_403_raises_forbidden_error(self): + body = json.dumps({"detail": "Missing required scopes"}) + with pytest.raises(ForbiddenResponseError): + _sync_you(_make_handler(403, body)).answer(query="test") + + def test_422_raises_unprocessable_entity_error(self): + body = json.dumps({"detail": [{"type": "missing", "loc": ["body", "query"], "msg": "Field required"}]}) + with pytest.raises(UnprocessableEntityResponseError) as exc_info: + _sync_you(_make_handler(422, body)).answer(query="") + # FastAPI validation format: detail array + assert exc_info.value.data.detail is not None + assert exc_info.value.data.detail[0]["type"] == "missing" + + def test_422_json_api_format(self): + """422 in JSON:API format {errors: [{status, code, title, detail}]}.""" + body = json.dumps({"errors": [{"status": "422", "code": "unprocessable_entity", "title": "Unprocessable Entity", "detail": "invalid request parameter(s)"}]}) + with pytest.raises(UnprocessableEntityResponseError) as exc_info: + _sync_you(_make_handler(422, body)).answer(query="") + assert exc_info.value.data.errors is not None + assert exc_info.value.data.errors[0]["code"] == "unprocessable_entity" + + def test_422_search_spec_format(self): + """422 in search spec format {error: string}.""" + body = json.dumps({"error": "invalid request parameter(s)"}) + with pytest.raises(UnprocessableEntityResponseError) as exc_info: + _sync_you(_make_handler(422, body)).answer(query="") + assert exc_info.value.data.error == "invalid request parameter(s)" + + def test_500_with_json_api_errors(self): + """500 in JSON:API format {errors: [...]}.""" + body = json.dumps({"errors": [{"status": "500", "code": "internal_server_error", "title": "Internal Server Error"}]}) + with pytest.raises(InternalServerErrorResponse) as exc_info: + _sync_you(_make_handler(500, body)).answer(query="test") + assert exc_info.value.data.errors is not None + assert exc_info.value.data.errors[0]["code"] == "internal_server_error" + + def test_4xx_fallback_raises_default_error(self): + body = json.dumps({"detail": "rate limited"}) + with pytest.raises(YouDefaultError): + _sync_you(_make_handler(429, body)).answer(query="test") + + @pytest.mark.asyncio + async def test_async_402_raises_payment_required_error(self): + body = json.dumps({ + "error": "payment_required", + "message": "Insufficient credits", + "upgrade_url": "https://you.com/platform", + }) + with pytest.raises(PaymentRequiredResponseError) as exc_info: + await _async_you(_make_handler(402, body)).answer_async(query="test") + assert exc_info.value.status_code == 402 + assert exc_info.value.data.error == "payment_required" + + @pytest.mark.asyncio + async def test_async_500_raises_internal_server_error(self): + body = json.dumps({"detail": "internal server error"}) + with pytest.raises(InternalServerErrorResponse): + await _async_you(_make_handler(500, body)).answer_async(query="test") diff --git a/tests/test_client.py b/tests/test_client.py index 229e12c..2bb4f61 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,7 +7,7 @@ def create_test_http_client(test_name: str) -> httpx.Client: return httpx.Client( headers={ - "x-speakeasy-test-name": test_name, - "x-speakeasy-test-instance-id": str(uuid.uuid4()), + "x-test-name": test_name, + "x-test-instance-id": str(uuid.uuid4()), } ) diff --git a/tests/test_contents.py b/tests/test_contents.py index 87ce0fa..3b108ee 100644 --- a/tests/test_contents.py +++ b/tests/test_contents.py @@ -26,7 +26,7 @@ def test_html_format(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.python.org", "https://www.example.com"], formats=[ContentsFormats.HTML], server_url=server_url, @@ -41,7 +41,7 @@ def test_markdown_format(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.MARKDOWN], server_url=server_url, @@ -55,7 +55,7 @@ def test_metadata_format(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.METADATA], server_url=server_url, @@ -71,7 +71,7 @@ def test_multiple_formats(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.HTML, ContentsFormats.MARKDOWN, ContentsFormats.METADATA], server_url=server_url, @@ -85,7 +85,7 @@ def test_multiple_urls(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=[ "https://www.you.com", "https://www.github.com", @@ -103,7 +103,7 @@ def test_single_url(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.example.com"], formats=[ContentsFormats.HTML], server_url=server_url, @@ -117,7 +117,7 @@ def test_without_formats(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.example.com"], server_url=server_url, ) @@ -130,7 +130,7 @@ def test_crawl_timeout(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.example.com"], formats=[ContentsFormats.HTML], crawl_timeout=30, # Set timeout to 30 seconds @@ -145,7 +145,7 @@ def test_max_age(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.example.com"], formats=[ContentsFormats.MARKDOWN], max_age=86400, # 1 day in seconds @@ -162,7 +162,7 @@ def test_unauthorized(self, server_url): with You(server_url=server_url, client=client, api_key_auth="invalid") as you: with pytest.raises(ContentsUnauthorizedError): - you.contents.generate( + you.contents( urls=["https://www.example.com"], server_url=server_url, ) @@ -172,7 +172,7 @@ def test_forbidden(self, server_url, api_key): with You(server_url=server_url, client=client, api_key_auth=api_key) as you: with pytest.raises(ContentsForbiddenError): - you.contents.generate( + you.contents( urls=["https://www.example.com"], server_url=server_url, ) @@ -181,7 +181,7 @@ def test_empty_urls(self, server_url, api_key): client = create_test_http_client("post_/v1/contents") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.contents.generate( + res = you.contents( urls=[], formats=[ContentsFormats.HTML], server_url=server_url, diff --git a/tests/test_direct_methods.py b/tests/test_direct_methods.py new file mode 100644 index 0000000..75f6e82 --- /dev/null +++ b/tests/test_direct_methods.py @@ -0,0 +1,152 @@ +"""Tests for the direct methods on You. + +Verifies that you.contents() and you.search() hit the +correct endpoints, pass params correctly, and work in both sync and +async modes. Uses httpx.MockTransport (no live server required). +""" + +import json + +import httpx +import pytest + +from youdotcom import You +from youdotcom.models import ( + ContentsResponse, + SearchResponse, +) + + +_SEARCH_BODY = json.dumps( + {"results": {"web": [{"title": "Test", "url": "https://example.com"}]}} +) +_CONTENTS_BODY = json.dumps( + [{"url": "https://example.com", "html": "

Hello

"}] +) + + +def _sync_you(handler, *, api_key: str | None = "test-key"): + kwargs: dict = { + "server_url": "http://mock.local", + "client": httpx.Client(transport=httpx.MockTransport(handler)), + } + if api_key is not None: + kwargs["api_key_auth"] = api_key + return You(**kwargs) + + +def _async_you(handler, *, api_key: str | None = "test-key"): + kwargs: dict = { + "server_url": "http://mock.local", + "async_client": httpx.AsyncClient(transport=httpx.MockTransport(handler)), + } + if api_key is not None: + kwargs["api_key_auth"] = api_key + return You(**kwargs) + + +# --------------------------------------------------------------------------- +# you.search() — POST /v1/search +# --------------------------------------------------------------------------- + + +class TestSearchDirect: + def test_returns_search_response(self): + res = _sync_you(lambda req: httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + )).search(query="python") + assert isinstance(res, SearchResponse) + assert res.results is not None + assert res.results.web is not None + assert len(res.results.web) == 1 + + def test_posts_to_search_endpoint(self): + captured: dict = {} + + def handler(request): + captured["url"] = str(request.url) + captured["method"] = request.method + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + ) + + _sync_you(handler).search(query="test") + assert captured["method"] == "POST" + assert "/v1/search" in captured["url"] + + def test_passes_params_in_body(self): + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + ) + + _sync_you(handler).search( + query="ai news", + count=5, + freshness="week", + country="US", + include_domains=["nature.com"], + ) + assert captured["body"]["query"] == "ai news" + assert captured["body"]["count"] == 5 + assert captured["body"]["freshness"] == "week" + assert captured["body"]["include_domains"] == ["nature.com"] + + @pytest.mark.asyncio + async def test_async_returns_search_response(self): + res = await _async_you(lambda req: httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + )).search_async(query="python") + assert isinstance(res, SearchResponse) + + +# --------------------------------------------------------------------------- +# you.contents() — POST /v1/contents +# --------------------------------------------------------------------------- + + +class TestContentsDirect: + def test_returns_contents_response_list(self): + res = _sync_you(lambda req: httpx.Response( + 200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY + )).contents(urls=["https://example.com"]) + assert isinstance(res, list) + assert isinstance(res[0], ContentsResponse) + assert res[0].url == "https://example.com" + + def test_posts_to_contents_endpoint(self): + captured: dict = {} + + def handler(request): + captured["url"] = str(request.url) + captured["method"] = request.method + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY + ) + + _sync_you(handler).contents(urls=["https://example.com"]) + assert captured["method"] == "POST" + assert "/v1/contents" in captured["url"] + + def test_passes_urls_in_body(self): + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY + ) + + _sync_you(handler).contents(urls=["https://example.com", "https://python.org"]) + assert captured["body"]["urls"] == ["https://example.com", "https://python.org"] + + @pytest.mark.asyncio + async def test_async_returns_contents_response_list(self): + res = await _async_you(lambda req: httpx.Response( + 200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY + )).contents_async(urls=["https://example.com"]) + assert isinstance(res, list) + assert isinstance(res[0], ContentsResponse) diff --git a/tests/test_live.py b/tests/test_live.py index ebe9304..3781cd9 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -26,18 +26,14 @@ LiveCrawl, LiveCrawlFormats, SafeSearch, - ExpressAgentRunsRequest, - AdvancedAgentRunsRequest, - ResearchTool, - SearchEffort, - ReportVerbosity, - AgentRunsBatchResponse, ResearchEffort, ResearchResponse, TaskResponse, TaskDetail, FinanceResearchEffort, + AnswerResponse, ) + from youdotcom.research_helpers import ( research_background, poll_research_task, @@ -51,11 +47,11 @@ ) -# Skip all tests in this file if no API key is provided. +# Skip keyed tests if no API key is provided. # Mirror the SDK's own env-var precedence (YDC_API_KEY first, then # YOU_API_KEY_AUTH as the documented 2.3.x fallback) so users on the # fallback env var don't get their live suite silently skipped. -pytestmark = pytest.mark.skipif( +requires_api_key = pytest.mark.skipif( not (os.getenv("YDC_API_KEY") or os.getenv("YOU_API_KEY_AUTH")), reason="YDC_API_KEY or YOU_API_KEY_AUTH environment variable not set" ) @@ -85,13 +81,14 @@ def you_client(api_key): ) +@requires_api_key class TestLiveSearch: """Live tests for the Search API.""" def test_basic_search(self, you_client): """Test basic search functionality against live API.""" with you_client as you: - res = you.search.unified(query="Python programming language") + res = you.search(query="Python programming language") assert res.results is not None assert res.metadata is not None @@ -102,7 +99,7 @@ def test_basic_search(self, you_client): def test_search_with_filters(self, you_client): """Test search with filters against live API.""" with you_client as you: - res = you.search.unified( + res = you.search( query="artificial intelligence", count=5, freshness=Freshness.WEEK, @@ -119,7 +116,7 @@ def test_search_with_filters(self, you_client): def test_search_with_livecrawl_web(self, you_client): """Test search with livecrawl for web results.""" with you_client as you: - res = you.search.unified( + res = you.search( query="machine learning tutorials", count=3, livecrawl=LiveCrawl.WEB, @@ -134,12 +131,13 @@ def test_search_with_livecrawl_web(self, you_client): # Check that we can access the contents field if result.contents: # At least one of html or markdown should be present - assert result.contents.markdown or result.contents.html + # (API may return empty string for some URLs) + assert result.contents.markdown is not None or result.contents.html is not None def test_search_with_livecrawl_news(self, you_client): """Test search with livecrawl for news results (new in 2.2.0).""" with you_client as you: - res = you.search.unified( + res = you.search( query="technology news today", count=3, livecrawl=LiveCrawl.NEWS, @@ -159,7 +157,7 @@ def test_search_with_livecrawl_news(self, you_client): def test_search_with_livecrawl_all(self, you_client): """Test search with livecrawl=ALL for both web and news.""" with you_client as you: - res = you.search.unified( + res = you.search( query="breaking tech news", count=3, livecrawl=LiveCrawl.ALL, @@ -167,33 +165,16 @@ def test_search_with_livecrawl_all(self, you_client): ) assert res.results is not None - - # Both web and news should be able to have contents - has_any_contents = False - - if res.results.web: - for result in res.results.web: - if result.contents: - has_any_contents = True - break - - if res.results.news: - for news_item in res.results.news: - if news_item.contents: - has_any_contents = True - break - - # We expect at least some results to have contents with livecrawl=ALL - # (This assertion may be relaxed if the API doesn't always return contents) +@requires_api_key class TestLiveContents: """Live tests for the Contents API.""" def test_html_format(self, you_client): """Test fetching content in HTML format.""" with you_client as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.example.com"], formats=[ContentsFormats.HTML], ) @@ -208,7 +189,7 @@ def test_html_format(self, you_client): def test_markdown_format(self, you_client): """Test fetching content in Markdown format.""" with you_client as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.example.com"], formats=[ContentsFormats.MARKDOWN], ) @@ -219,7 +200,7 @@ def test_markdown_format(self, you_client): def test_metadata_format(self, you_client): """Test fetching metadata from a page.""" with you_client as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.METADATA], ) @@ -232,7 +213,7 @@ def test_metadata_format(self, you_client): def test_multiple_formats(self, you_client): """Test fetching multiple formats at once.""" with you_client as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.example.com"], formats=[ContentsFormats.HTML, ContentsFormats.MARKDOWN], ) @@ -241,42 +222,7 @@ def test_multiple_formats(self, you_client): assert len(res) > 0 -class TestLiveAgents: - """Live tests for the Agents API.""" - - def test_express_agent(self, you_client): - """Test Express agent with basic query.""" - with you_client as you: - res = you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="What is the capital of France?", - stream=False, - ) - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - assert len(res.output) > 0 - - @pytest.mark.slow - def test_advanced_agent_with_research(self, you_client): - """Test Advanced agent with ResearchTool.""" - with you_client as you: - res = you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="What are the latest developments in AI?", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.LOW, - report_verbosity=ReportVerbosity.MEDIUM, - )], - ) - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - - +@requires_api_key class TestLiveResearch: """Live tests for the Research API (new in 2.3.0).""" @@ -338,6 +284,7 @@ def test_research_with_sources(self, you_client): assert source.url is not None +@requires_api_key class TestLiveResearchOutputSchema: """Live test for Research `output_schema` parameter (beta feature). @@ -375,6 +322,7 @@ def test_research_output_schema_structured_payload(self, you_client): assert "same_entity" in res.output.content +@requires_api_key class TestLiveResearchSourceControl: """Live test for Research `source_control` parameter (beta feature). @@ -399,6 +347,7 @@ def test_research_source_control_with_boost_domains(self, you_client): assert len(res.output.content) > 0 +@requires_api_key class TestLiveFinanceResearch: """Live tests for the Finance Research API.""" @@ -420,28 +369,8 @@ def test_finance_research_basic(self, you_client): for source in res.output.sources: assert source.url is not None - def test_finance_research_lite_effort(self, you_client): - """Test finance_research with LITE effort returns a quick answer. - Skipped if the server hasn't deployed the lite tier yet.""" - with you_client as you: - try: - res = you.finance_research( - input="What was Apple's revenue in fiscal year 2024?", - research_effort=FinanceResearchEffort.LITE, - ) - except (FinanceResearchUnprocessableEntityError, YouDefaultError) as e: - if "lite" in str(e).lower() or "422" in str(e): - pytest.skip("Finance research lite tier not yet deployed on server") - raise - - assert res.output is not None - assert res.output.content is not None - assert len(res.output.content) > 0 - if res.output.sources: - for source in res.output.sources: - assert source.url is not None - +@requires_api_key class TestLiveContentsMaxAge: """Live test for Contents `max_age` parameter. @@ -452,7 +381,7 @@ class TestLiveContentsMaxAge: def test_contents_with_max_age(self, you_client): """max_age is accepted as an optional parameter.""" with you_client as you: - res = you.contents.generate( + res = you.contents( urls=["https://www.example.com"], formats=[ContentsFormats.MARKDOWN], max_age=86400, # 1 day @@ -462,6 +391,7 @@ def test_contents_with_max_age(self, you_client): assert len(res) > 0 +@requires_api_key class TestLiveSearchBoostDomains: """Live test for Search `boost_domains` parameter. @@ -469,10 +399,10 @@ class TestLiveSearchBoostDomains: other domains — for a more permissive alternative to `include_domains`. """ - def test_search_post_boost_domains_list(self, you_client): - """search_post accepts a Python list of boost domains.""" + def test_search_boost_domains_list(self, you_client): + """search accepts a Python list of boost domains.""" with you_client as you: - res = you.search_post( + res = you.search( query="Python type hints vs TypeScript inference", count=5, boost_domains=["python.org", "realpython.com"], @@ -499,6 +429,7 @@ def test_search_post_boost_domains_list(self, you_client): _BG_TIMEOUT_S = 120.0 # generous wall-clock for LITE background tasks +@requires_api_key class TestLiveResearchBackground: """Live tests for background-mode research (POST /v1/research?background=true).""" @@ -603,6 +534,7 @@ def test_research_and_wait(self, you_client): assert "output" in result_dump +@requires_api_key class TestLiveResearchBackgroundHelpers: """Live tests for the research_helpers convenience functions.""" @@ -669,6 +601,7 @@ def test_stream_research(self, you_client): # For a live test we use a simple query and a generous but bounded timeout. # Marked slow so it can be skipped with `-m "not slow"`. # --------------------------------------------------------------------------- +@requires_api_key class TestLiveResearchFrontier: """Live tests for frontier research effort (requires background=true).""" @@ -708,6 +641,78 @@ def test_frontier_without_background_raises_422(self, you_client): ) +# --------------------------------------------------------------------------- +# Answer API (new in 2.6.0) +# --------------------------------------------------------------------------- +@requires_api_key +class TestLiveAnswer: + """Live tests for the Answer API (POST /v1/answer). + + The Answer API returns a synthesized answer with citations and web results. + Requires an API key. + """ + + def test_basic_answer(self, you_client): + """Test basic answer query returns AnswerResponse with answer + citations.""" + with you_client as you: + res = you.answer(query="What is the capital of France?") + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + # Citations should be present for a factual query + assert len(res.citations) > 0 + for citation in res.citations: + assert citation.source is not None + assert len(citation.source) > 0 + # Web results should be present + assert len(res.results.web) > 0 + for result in res.results.web: + assert result.url is not None + assert result.title is not None + + def test_answer_with_freshness(self, you_client): + """Test answer with freshness filter.""" + with you_client as you: + res = you.answer( + query="Latest AI developments", + freshness="week", + ) + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + + def test_answer_with_country(self, you_client): + """Test answer with country filter.""" + with you_client as you: + res = you.answer( + query="Best restaurants in London", + country=Country.GB, + ) + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + + def test_answer_with_boost_domains(self, you_client): + """Test answer with boost_domains (can combine with exclude, not include).""" + with you_client as you: + res = you.answer( + query="Python type hints", + boost_domains=["python.org", "docs.python.org"], + ) + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + + @pytest.mark.asyncio + async def test_async_answer(self, you_client): + """Test async you.answer_async().""" + with you_client as you: + res = await you.answer_async(query="What is 2+2?") + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + + if __name__ == "__main__": # Run with: python -m pytest tests/test_live.py -v pytest.main([__file__, "-v"]) diff --git a/tests/test_performance.py b/tests/test_performance.py index 5e958eb..8443dc7 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -3,7 +3,6 @@ This module tests all supported endpoint combinations to measure SDK latency vs API latency: - Search: with various filters, livecrawl options, pagination -- Agents: different agent types, tool combinations, streaming vs non-streaming - Contents: different formats, single vs multiple URLs Environment variables: @@ -32,9 +31,6 @@ from tests.timing_client import SDKCallTiming, TimingHTTPClient from youdotcom import You from youdotcom.models import ( - ComputeTool, - ResearchTool, - WebSearchTool, Country, ContentsFormats, Freshness, @@ -42,10 +38,6 @@ LiveCrawl, LiveCrawlFormats, SafeSearch, - SearchEffort, - ReportVerbosity, - ExpressAgentRunsRequest, - AdvancedAgentRunsRequest, ) @@ -103,8 +95,8 @@ def create_timing_client(test_name: str) -> TimingHTTPClient: return TimingHTTPClient( follow_redirects=True, headers={ - "x-speakeasy-test-name": test_name, - "x-speakeasy-test-instance-id": str(uuid.uuid4()), + "x-test-name": test_name, + "x-test-instance-id": str(uuid.uuid4()), } ) @@ -168,11 +160,11 @@ class TestSearchPerformance: def test_search_basic(self, server_url, api_key, iterations, show_detailed): """Basic search with query only.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="latest AI developments", server_url=server_url) + you.search(query="latest AI developments", server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: basic query") ALL_METRICS.append(metrics) @@ -181,11 +173,11 @@ def call(): def test_search_with_count(self, server_url, api_key, iterations, show_detailed): """Search with result count limit.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="python programming", count=10, server_url=server_url) + you.search(query="python programming", count=10, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: with count=10") ALL_METRICS.append(metrics) @@ -194,11 +186,11 @@ def call(): def test_search_with_freshness_day(self, server_url, api_key, iterations, show_detailed): """Search with freshness filter (day).""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="breaking news", freshness=Freshness.DAY, server_url=server_url) + you.search(query="breaking news", freshness=Freshness.DAY, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: freshness=DAY") ALL_METRICS.append(metrics) @@ -207,11 +199,11 @@ def call(): def test_search_with_freshness_week(self, server_url, api_key, iterations, show_detailed): """Search with freshness filter (week).""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="renewable energy", freshness=Freshness.WEEK, server_url=server_url) + you.search(query="renewable energy", freshness=Freshness.WEEK, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: freshness=WEEK") ALL_METRICS.append(metrics) @@ -220,11 +212,11 @@ def call(): def test_search_with_country_us(self, server_url, api_key, iterations, show_detailed): """Search with country filter (US).""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="local restaurants", country=Country.US, server_url=server_url) + you.search(query="local restaurants", country=Country.US, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: country=US") ALL_METRICS.append(metrics) @@ -233,11 +225,11 @@ def call(): def test_search_with_country_gb(self, server_url, api_key, iterations, show_detailed): """Search with country filter (GB).""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="football news", country=Country.GB, server_url=server_url) + you.search(query="football news", country=Country.GB, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: country=GB") ALL_METRICS.append(metrics) @@ -246,11 +238,11 @@ def call(): def test_search_with_language_en(self, server_url, api_key, iterations, show_detailed): """Search with language filter (English).""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="machine learning", language=Language.EN, server_url=server_url) + you.search(query="machine learning", language=Language.EN, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: language=EN") ALL_METRICS.append(metrics) @@ -259,11 +251,11 @@ def call(): def test_search_with_language_es(self, server_url, api_key, iterations, show_detailed): """Search with language filter (Spanish).""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="tecnología", language=Language.ES, server_url=server_url) + you.search(query="tecnología", language=Language.ES, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: language=ES") ALL_METRICS.append(metrics) @@ -272,11 +264,11 @@ def call(): def test_search_with_safesearch_off(self, server_url, api_key, iterations, show_detailed): """Search with safesearch off.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="research", safesearch=SafeSearch.OFF, server_url=server_url) + you.search(query="research", safesearch=SafeSearch.OFF, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: safesearch=OFF") ALL_METRICS.append(metrics) @@ -285,11 +277,11 @@ def call(): def test_search_with_safesearch_moderate(self, server_url, api_key, iterations, show_detailed): """Search with safesearch moderate.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="family content", safesearch=SafeSearch.MODERATE, server_url=server_url) + you.search(query="family content", safesearch=SafeSearch.MODERATE, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: safesearch=MODERATE") ALL_METRICS.append(metrics) @@ -298,11 +290,11 @@ def call(): def test_search_with_safesearch_strict(self, server_url, api_key, iterations, show_detailed): """Search with safesearch strict.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="kids learning", safesearch=SafeSearch.STRICT, server_url=server_url) + you.search(query="kids learning", safesearch=SafeSearch.STRICT, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: safesearch=STRICT") ALL_METRICS.append(metrics) @@ -311,11 +303,11 @@ def call(): def test_search_with_pagination(self, server_url, api_key, iterations, show_detailed): """Search with pagination (offset).""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified(query="python tutorials", count=5, offset=2, server_url=server_url) + you.search(query="python tutorials", count=5, offset=2, server_url=server_url) metrics = measure_sdk_call(call, client, iterations, "Search: with pagination (offset=2)") ALL_METRICS.append(metrics) @@ -324,11 +316,11 @@ def call(): def test_search_with_livecrawl_web(self, server_url, api_key, iterations, show_detailed): """Search with livecrawl enabled for web results.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="machine learning tutorials", count=3, livecrawl=LiveCrawl.WEB, @@ -342,11 +334,11 @@ def call(): def test_search_with_livecrawl_news(self, server_url, api_key, iterations, show_detailed): """Search with livecrawl enabled for news results.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="tech news", count=3, livecrawl=LiveCrawl.NEWS, @@ -360,11 +352,11 @@ def call(): def test_search_with_livecrawl_all(self, server_url, api_key, iterations, show_detailed): """Search with livecrawl enabled for all results.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="quantum computing", count=3, livecrawl=LiveCrawl.ALL, @@ -378,11 +370,11 @@ def call(): def test_search_with_livecrawl_html(self, server_url, api_key, iterations, show_detailed): """Search with livecrawl returning HTML format.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="AI research", count=3, livecrawl=LiveCrawl.WEB, @@ -397,11 +389,11 @@ def call(): def test_search_with_livecrawl_markdown(self, server_url, api_key, iterations, show_detailed): """Search with livecrawl returning Markdown format.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="documentation guides", count=3, livecrawl=LiveCrawl.WEB, @@ -416,11 +408,11 @@ def call(): def test_search_with_all_filters(self, server_url, api_key, iterations, show_detailed): """Search with multiple filters combined.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="quantum computing research", count=10, freshness=Freshness.MONTH, @@ -438,11 +430,11 @@ def call(): def test_search_with_filters_and_livecrawl(self, server_url, api_key, iterations, show_detailed): """Search with filters and livecrawl combined.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="AI developments", count=5, freshness=Freshness.WEEK, @@ -459,11 +451,11 @@ def call(): def test_search_with_news_livecrawl(self, server_url, api_key, iterations, show_detailed): """Search with livecrawl for news results (news now supports contents).""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="technology news", count=5, livecrawl=LiveCrawl.NEWS, @@ -478,11 +470,11 @@ def call(): def test_search_with_livecrawl_all_news_contents(self, server_url, api_key, iterations, show_detailed): """Search with livecrawl=ALL for both web and news contents.""" - client = create_timing_client("get_/v1/search") + client = create_timing_client("post_/v1/search") with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.search.unified( + you.search( query="breaking tech news", count=3, livecrawl=LiveCrawl.ALL, @@ -496,354 +488,6 @@ def call(): print_detailed_metrics(metrics) -# ============================================================================ -# Agents Endpoint Tests -# ============================================================================ - -class TestAgentsPerformance: - """Performance tests for the Agents API.""" - - def test_agents_express_no_tools(self, server_url, api_key, iterations, show_detailed): - """Express agent without tools (non-streaming).""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="Teach me how to make an omelet", - stream=False, - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: EXPRESS, no tools") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_express_with_websearch(self, server_url, api_key, iterations, show_detailed): - """Express agent with WebSearchTool.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="What are the latest AI developments?", - stream=False, - tools=[WebSearchTool()], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: EXPRESS + WebSearchTool") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_express_with_websearch_force(self, server_url, api_key, iterations, show_detailed): - """Express agent with WebSearchTool.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="Tell me about Python", - stream=False, - tools=[WebSearchTool()], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: EXPRESS + WebSearchTool") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_no_tools(self, server_url, api_key, iterations, show_detailed): - """Advanced agent without tools.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Explain quantum entanglement", - stream=False, - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED, no tools") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_research(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ResearchTool.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Research the latest breakthroughs in quantum computing", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.AUTO, - report_verbosity=ReportVerbosity.MEDIUM, - )], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + ResearchTool") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_research_low_effort(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ResearchTool (low search effort).""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Quick research on AI", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.LOW, - report_verbosity=ReportVerbosity.MEDIUM, - )], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + ResearchTool (low effort)") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_research_high_effort(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ResearchTool (high search effort).""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Deep research on climate change", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.HIGH, - report_verbosity=ReportVerbosity.HIGH, - )], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + ResearchTool (high effort)") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_research_verbosity_low(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ResearchTool (low verbosity).""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Brief summary of AI trends", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.LOW, - report_verbosity=ReportVerbosity.MEDIUM, - )], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + ResearchTool (low verbosity)") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_research_verbosity_high(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ResearchTool (high verbosity).""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Detailed analysis of blockchain", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.HIGH, - report_verbosity=ReportVerbosity.HIGH, - )], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + ResearchTool (high verbosity)") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_compute(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ComputeTool.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Calculate the square root of 169", - stream=False, - tools=[ComputeTool()], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + ComputeTool") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_websearch_and_research(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ResearchTool.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Find and research AI startups", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.AUTO, - report_verbosity=ReportVerbosity.MEDIUM, - )], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + Research") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_websearch_and_compute(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ComputeTool.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Find stock prices and calculate averages", - stream=False, - tools=[ComputeTool()], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + Compute") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_research_and_compute(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with ResearchTool + ComputeTool.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Research market trends and calculate growth rates", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.AUTO, - report_verbosity=ReportVerbosity.MEDIUM, - ), ComputeTool()], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + Research + Compute") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_advanced_with_all_tools(self, server_url, api_key, iterations, show_detailed): - """Advanced agent with all tools.""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Research tech trends, find data, and calculate statistics", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.AUTO, - report_verbosity=ReportVerbosity.MEDIUM, - ), ComputeTool()], - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: ADVANCED + all tools") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_express_verbosity_low(self, server_url, api_key, iterations, show_detailed): - """Express agent (verbosity not supported for express).""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="Brief overview of Python", - stream=False, - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: EXPRESS") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - def test_agents_express_verbosity_high(self, server_url, api_key, iterations, show_detailed): - """Express agent (verbosity not supported for express).""" - client = create_timing_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - def call(): - you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="Detailed explanation of Python", - stream=False, - ), - server_url=server_url, - ) - - metrics = measure_sdk_call(call, client, iterations, "Agents: EXPRESS") - ALL_METRICS.append(metrics) - if show_detailed: - print_detailed_metrics(metrics) - - # ============================================================================ # Contents Endpoint Tests # ============================================================================ @@ -857,7 +501,7 @@ def test_contents_single_url_html(self, server_url, api_key, iterations, show_de with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.contents.generate( + you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.HTML], server_url=server_url, @@ -874,7 +518,7 @@ def test_contents_single_url_markdown(self, server_url, api_key, iterations, sho with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.contents.generate( + you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.MARKDOWN], server_url=server_url, @@ -891,7 +535,7 @@ def test_contents_single_url_metadata(self, server_url, api_key, iterations, sho with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.contents.generate( + you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.METADATA], server_url=server_url, @@ -908,7 +552,7 @@ def test_contents_multiple_formats(self, server_url, api_key, iterations, show_d with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.contents.generate( + you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.HTML, ContentsFormats.MARKDOWN, ContentsFormats.METADATA], server_url=server_url, @@ -925,7 +569,7 @@ def test_contents_with_crawl_timeout(self, server_url, api_key, iterations, show with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.contents.generate( + you.contents( urls=["https://www.python.org"], formats=[ContentsFormats.HTML], crawl_timeout=30, @@ -943,7 +587,7 @@ def test_contents_multiple_urls_html(self, server_url, api_key, iterations, show with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.contents.generate( + you.contents( urls=[ "https://www.python.org", "https://www.github.com", @@ -964,7 +608,7 @@ def test_contents_multiple_urls_markdown(self, server_url, api_key, iterations, with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.contents.generate( + you.contents( urls=[ "https://www.python.org", "https://www.github.com", @@ -985,7 +629,7 @@ def test_contents_many_urls_html(self, server_url, api_key, iterations, show_det with You(server_url=server_url, client=client, api_key_auth=api_key) as you: def call(): - you.contents.generate( + you.contents( urls=[ "https://www.python.org", "https://www.github.com", diff --git a/tests/test_research.py b/tests/test_research.py index 84a4919..a3318d3 100644 --- a/tests/test_research.py +++ b/tests/test_research.py @@ -134,8 +134,8 @@ class TestResearchAsync: async def test_basic_research_async(self, server_url, api_key): async_client = httpx.AsyncClient( headers={ - "x-speakeasy-test-name": "post_/v1/research", - "x-speakeasy-test-instance-id": str(uuid.uuid4()), + "x-test-name": "post_/v1/research", + "x-test-instance-id": str(uuid.uuid4()), }, follow_redirects=True, ) @@ -216,20 +216,6 @@ def test_basic_finance_research(self, server_url, api_key): assert source.url is not None assert source.title is not None - def test_finance_research_lite_effort(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 Apple's revenue in FY2024?", - research_effort=FinanceResearchEffort.LITE, - server_url=server_url, - ) - - assert res.output is not None - assert res.output.content is not None - assert "effort: lite" in res.output.content - def test_finance_research_unauthorized(self, server_url): client = create_test_http_client("post_/v1/finance_research-unauthorized") @@ -306,8 +292,8 @@ def test_research_background_false_returns_research_response(self, server_url, a async def test_get_research_task_async_returns_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()), + "x-test-name": "get_/v1/research/{task_id}", + "x-test-instance-id": str(uuid.uuid4()), }, follow_redirects=True, ) diff --git a/tests/test_research_helpers.py b/tests/test_research_helpers.py index 9a9a7e1..5013687 100644 --- a/tests/test_research_helpers.py +++ b/tests/test_research_helpers.py @@ -175,8 +175,8 @@ def test_research_background_returns_task_response(self, server_url, api_key): 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()), + "x-test-name": "post_/v1/research-background", + "x-test-instance-id": str(uuid.uuid4()), }, follow_redirects=True, ) @@ -292,8 +292,8 @@ def test_poll_returns_completed_task_detail(self, server_url, api_key): 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()), + "x-test-name": "get_/v1/research/{task_id}", + "x-test-instance-id": str(uuid.uuid4()), }, follow_redirects=True, ) @@ -731,9 +731,9 @@ def handler(request): 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"). + # (research.searching) that's NOT in the documented enum. A strict + # decoder would raise ValidationError on it; our tolerant helper must + # surface it as RawStreamEvent(event="research.searching"). recorded_ua: dict = {} def record_send(request): @@ -756,8 +756,8 @@ def record_send(request): 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()), + "x-test-name": "get_/v1/research/{task_id}/stream", + "x-test-instance-id": str(uuid.uuid4()), }, ) you = You( @@ -779,7 +779,7 @@ def record_send(request): 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). + # (BaseSDK._build_request sets it directly). assert recorded_ua["value"] == f"youdotcom-python-sdk/{you.sdk_configuration.sdk_version}" # --------------------------------------------------------------------------- # _resolve_default_timeout: auto-adjust timeout based on research_effort. diff --git a/tests/test_runs.py b/tests/test_runs.py deleted file mode 100644 index 3a949f9..0000000 --- a/tests/test_runs.py +++ /dev/null @@ -1,256 +0,0 @@ -import os -import pytest - -from tests.test_client import create_test_http_client -from youdotcom import You -from youdotcom.errors import ( - AgentRuns400ResponseError, - AgentRuns401ResponseError, - YouDefaultError, -) -from youdotcom.models import ( - ComputeTool, - ResearchTool, - WebSearchTool, - ExpressAgentRunsRequest, - AdvancedAgentRunsRequest, - CustomAgentRunsRequest, - SearchEffort, - ReportVerbosity, - AgentRunsBatchResponse, -) -from youdotcom.utils import eventstreaming - - -@pytest.fixture -def server_url(): - return os.getenv("TEST_SERVER_URL", "http://localhost:18080") - - -@pytest.fixture -def api_key(): - return "test-api-key" - - -class TestExpressAgent: - def test_basic(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="Teach me how to make an omelet", - stream=False, - ), - server_url=server_url, - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - assert isinstance(res.output, list) - assert len(res.output) > 0 - - def test_streaming(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="Teach me how to make an omelet", - stream=True, - ), - server_url=server_url, - ) - - # Mock server returns batch response even for streaming requests - # In production, this would be an EventStream - assert res is not None - - def test_with_web_search_tool(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="Summarize today's top AI research headlines.", - stream=False, - tools=[WebSearchTool()], - ), - server_url=server_url, - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - - -class TestAdvancedAgent: - def test_with_research_tool(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Summarize today's top AI research headlines.", - stream=False, - tools=[ResearchTool( - search_effort=SearchEffort.AUTO, - report_verbosity=ReportVerbosity.MEDIUM, - )], - ), - server_url=server_url, - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - - def test_with_compute_tool(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Calculate 15 * 23 and explain the steps.", - stream=False, - tools=[ComputeTool()], - ), - server_url=server_url, - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - - def test_with_multiple_tools(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Research and calculate the square root of 169.", - stream=True, - tools=[ - ComputeTool(), - ResearchTool( - search_effort=SearchEffort.AUTO, - report_verbosity=ReportVerbosity.HIGH, - ), - ], - ), - server_url=server_url, - ) - - # Mock server returns batch response even for streaming requests - # In production, this would be an EventStream - assert res is not None - - def test_research_tool_configuration(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=AdvancedAgentRunsRequest( - input="Research quantum computing breakthroughs.", - stream=False, - tools=[ - ResearchTool( - search_effort=SearchEffort.HIGH, - report_verbosity=ReportVerbosity.MEDIUM, - ), - ], - ), - server_url=server_url, - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - - -class TestCustomAgent: - def test_with_uuid(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=CustomAgentRunsRequest( - agent="c12fa027-424e-4002-9659-746c16e74faa", - input="Teach me how to make an omelet", - stream=False, - ), - server_url=server_url, - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - - def test_with_tools(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=CustomAgentRunsRequest( - agent="c12fa027-424e-4002-9659-746c16e74faa", - input="Search for Python best practices.", - stream=False, - tools=[WebSearchTool()], - ), - server_url=server_url, - ) - - assert isinstance(res, AgentRunsBatchResponse) - assert res.output is not None - - -class TestRunsErrors: - def test_unauthorized(self, server_url): - client = create_test_http_client("post_/v1/agents/runs-unauthorized") - - with You(server_url=server_url, client=client, api_key_auth="invalid") as you: - with pytest.raises(AgentRuns401ResponseError): - you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="test", - stream=False, - ), - server_url=server_url, - ) - - def test_forbidden(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs-forbidden") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - # Mock server returns 403 which gets caught as a default error - # In production API, this would be a more specific error type - with pytest.raises(YouDefaultError): - you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="test", - stream=False, - ), - server_url=server_url, - ) - - def test_bad_request(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs-bad-request") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - with pytest.raises((AgentRuns400ResponseError, YouDefaultError)): - you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="test", - stream=False, - ), - server_url=server_url, - ) - - def test_empty_input(self, server_url, api_key): - client = create_test_http_client("post_/v1/agents/runs") - - with You(server_url=server_url, client=client, api_key_auth=api_key) as you: - res = you.agents.runs.create( - request=ExpressAgentRunsRequest( - input="", - stream=False, - ), - server_url=server_url, - ) - - assert res is not None diff --git a/tests/test_search.py b/tests/test_search.py index 1374717..10de8ed 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,204 +1,32 @@ -import os import json import pytest import httpx -from tests.test_client import create_test_http_client from youdotcom import You from youdotcom.errors import ( ForbiddenResponseError, + InternalServerErrorResponse, UnauthorizedResponseError, UnprocessableEntityResponseError, YouDefaultError, ) -from youdotcom.models import ( - Country, - Freshness, - LiveCrawl, - LiveCrawlFormats, - SafeSearch, -) - - -@pytest.fixture -def server_url(): - return os.getenv("TEST_SERVER_URL", "http://localhost:18080") - - -@pytest.fixture -def api_key(): - return "test-api-key" - - -class TestSearchBasic: - def test_basic_search(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="latest AI developments", server_url=server_url) - - assert res.results is not None - assert res.metadata is not None - assert res.metadata.query is not None - assert res.results.web or res.results.news - - -class TestSearchFilters: - def test_search_with_filters(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="renewable energy", - count=10, - freshness=Freshness.WEEK, - country=Country.US, - safesearch=SafeSearch.MODERATE, - server_url=server_url, - ) - - assert res.results is not None - assert res.metadata is not None - - def test_search_with_pagination(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="python programming", - count=5, - offset=1, - server_url=server_url, - ) - - assert res.results is not None - assert res.metadata is not None - - 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], - server_url=server_url, - ) - - assert res.results is not None - - if res.results.web: - for result in res.results.web: - if hasattr(result, "contents") and result.contents: - assert result.contents.markdown is not None - - 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", - count=20, - offset=0, - freshness=Freshness.MONTH, - country=Country.GB, - safesearch=SafeSearch.STRICT, - livecrawl=LiveCrawl.WEB, - livecrawl_formats=[LiveCrawlFormats.HTML], - server_url=server_url, - ) - - assert res.results is not None - assert res.metadata is not None - - if res.results.web: - for result in res.results.web: - if hasattr(result, "contents") and result.contents: - assert result.contents.html is not None - - 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], - server_url=server_url, - ) - - assert res.results is not None - - # News results can now have contents field when livecrawl is enabled - if res.results.news: - for news_item in res.results.news: - # Contents field is optional but should be accessible - if hasattr(news_item, "contents") and news_item.contents: - # If contents exists, it should have markdown when requested - assert news_item.contents.markdown is not None or news_item.contents.html is not None - - 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], - server_url=server_url, - ) - - assert res.results is not None - - # Both web and news can have contents with livecrawl=ALL - if res.results.web: - for result in res.results.web: - if hasattr(result, "contents") and result.contents: - assert result.contents.html is not None - - if res.results.news: - for news_item in res.results.news: - if hasattr(news_item, "contents") and news_item.contents: - assert news_item.contents.html is not None - - -class TestSearchErrors: - 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((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((ForbiddenResponseError, YouDefaultError)): - you.search.unified(query="test", server_url=server_url) +from youdotcom.models import SearchResponse # --------------------------------------------------------------------------- -# POST-side error tests: search_post() must raise the same consolidated -# *ResponseError classes as search.unified() (GET). Uses MockTransport -# because the mockserver has no POST /v1/search handler. +# Error tests: search() (POST /v1/search) must raise the +# consolidated *ResponseError classes. Uses MockTransport. # --------------------------------------------------------------------------- -class TestSearchPostErrors: - """Verify search_post() raises the consolidated *ResponseError classes. +class TestSearchErrors: + """Verify search() raises the consolidated *ResponseError classes. - The CHANGELOG documents that both Search endpoints (GET and POST) raise - the consolidated error classes. These tests lock that contract for the - POST path so a regen that mis-wires POST errors would fail CI. + These tests lock the error-class contract so a regen that mis-wires + errors would fail CI. Each test asserts the specific error class. """ - def test_post_unauthorized(self): + def test_unauthorized(self): def handler(request): return httpx.Response( 401, @@ -209,11 +37,11 @@ def handler(request): transport = httpx.MockTransport(handler) sdk_client = httpx.Client(transport=transport) you = You(server_url="http://mock.local", client=sdk_client, api_key_auth="invalid") - with pytest.raises((UnauthorizedResponseError, YouDefaultError)): - you.search_post(query="test") + with pytest.raises(UnauthorizedResponseError): + you.search(query="test") sdk_client.close() - def test_post_forbidden(self): + def test_forbidden(self): def handler(request): return httpx.Response( 403, @@ -224,11 +52,11 @@ def handler(request): transport = httpx.MockTransport(handler) sdk_client = httpx.Client(transport=transport) you = You(server_url="http://mock.local", client=sdk_client, api_key_auth="test") - with pytest.raises((ForbiddenResponseError, YouDefaultError)): - you.search_post(query="test") + with pytest.raises(ForbiddenResponseError): + you.search(query="test") sdk_client.close() - def test_post_unprocessable(self): + def test_unprocessable(self): def handler(request): return httpx.Response( 422, @@ -239,19 +67,81 @@ def handler(request): transport = httpx.MockTransport(handler) sdk_client = httpx.Client(transport=transport) you = You(server_url="http://mock.local", client=sdk_client, api_key_auth="test") - with pytest.raises((UnprocessableEntityResponseError, YouDefaultError)): - you.search_post( + with pytest.raises(UnprocessableEntityResponseError): + you.search( query="test", include_domains=["example.com"], exclude_domains=["spam.com"], ) sdk_client.close() + def test_internal_server_error(self): + def handler(request): + return httpx.Response( + 500, + headers={"content-type": "application/json"}, + content=json.dumps({"detail": "internal server error"}), + ) + + transport = httpx.MockTransport(handler) + sdk_client = httpx.Client(transport=transport) + you = You(server_url="http://mock.local", client=sdk_client, api_key_auth="test") + with pytest.raises(InternalServerErrorResponse): + you.search(query="test") + sdk_client.close() + + def test_4xx_fallback_raises_default_error(self): + def handler(request): + return httpx.Response( + 429, + headers={"content-type": "application/json"}, + content=json.dumps({"detail": "rate limited"}), + ) + + transport = httpx.MockTransport(handler) + sdk_client = httpx.Client(transport=transport) + you = You(server_url="http://mock.local", client=sdk_client, api_key_auth="test") + with pytest.raises(YouDefaultError): + you.search(query="test") + sdk_client.close() + + @pytest.mark.asyncio + async def test_async_unauthorized(self): + def handler(request): + return httpx.Response( + 401, + headers={"content-type": "application/json"}, + content=json.dumps({"detail": "unauthorized"}), + ) + + transport = httpx.MockTransport(handler) + async_client = httpx.AsyncClient(transport=transport) + you = You(server_url="http://mock.local", async_client=async_client, api_key_auth="bad-key") + with pytest.raises(UnauthorizedResponseError): + await you.search_async(query="test") + await async_client.aclose() + + @pytest.mark.asyncio + async def test_async_internal_server_error(self): + def handler(request): + return httpx.Response( + 500, + headers={"content-type": "application/json"}, + content=json.dumps({"detail": "internal server error"}), + ) + + transport = httpx.MockTransport(handler) + async_client = httpx.AsyncClient(transport=transport) + you = You(server_url="http://mock.local", async_client=async_client, api_key_auth="test") + with pytest.raises(InternalServerErrorResponse): + await you.search_async(query="test") + await async_client.aclose() -class TestSearchPostBoostDomains: - """Verify search_post() forwards boost_domains in the request body.""" - def test_post_boost_domains_forwarded(self): +class TestSearchBoostDomains: + """Verify search() forwards boost_domains in the request body.""" + + def test_boost_domains_forwarded(self): def handler(request): body = json.loads(request.content) assert "boost_domains" in body @@ -272,10 +162,52 @@ def handler(request): transport = httpx.MockTransport(handler) sdk_client = httpx.Client(transport=transport) you = You(server_url="http://mock.local", client=sdk_client, api_key_auth="test") - res = you.search_post( + res = you.search( query="Python type hints", boost_domains=["python.org", "realpython.com"], ) assert res.results is not None sdk_client.close() + +class TestSearchSuccess: + """Verify search() returns SearchResponse and hits POST /v1/search.""" + + _SEARCH_BODY = json.dumps( + {"results": {"web": [{"title": "Test Result", "url": "https://example.com"}]}} + ) + + def test_search_returns_search_response(self): + def handler(request): + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=self._SEARCH_BODY + ) + + transport = httpx.MockTransport(handler) + sdk_client = httpx.Client(transport=transport) + you = You(server_url="http://mock.local", client=sdk_client, api_key_auth="test-key") + res = you.search(query="python", count=5) + assert isinstance(res, SearchResponse) + assert res.results is not None + assert res.results.web is not None + assert len(res.results.web) == 1 + assert res.results.web[0].title == "Test Result" + sdk_client.close() + + def test_posts_to_search_endpoint(self): + captured: dict = {} + + def handler(request): + captured["url"] = str(request.url) + captured["method"] = request.method + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=self._SEARCH_BODY + ) + + transport = httpx.MockTransport(handler) + sdk_client = httpx.Client(transport=transport) + you = You(server_url="http://mock.local", client=sdk_client, api_key_auth="test-key") + you.search(query="python") + assert captured["method"] == "POST" + assert "/v1/search" in captured["url"] + sdk_client.close() diff --git a/tests/test_shims.py b/tests/test_shims.py new file mode 100644 index 0000000..505468f --- /dev/null +++ b/tests/test_shims.py @@ -0,0 +1,94 @@ +"""Tests for backward-compat sub-SDK shims with DeprecationWarning.""" + +import json +import warnings + +import httpx +import pytest + +from youdotcom import You +from youdotcom.models import SearchResponse + + +_SEARCH_BODY = json.dumps( + {"results": {"web": [{"title": "Test", "url": "https://example.com"}]}} +) +_CONTENTS_BODY = json.dumps([{"url": "https://example.com", "html": "

Hi

"}]) + + +def _you(handler, api_key="test"): + return You( + server_url="http://mock.local", + client=httpx.Client(transport=httpx.MockTransport(handler)), + api_key_auth=api_key, + ) + + +class TestSearchShim: + def test_direct_call_no_warning(self): + you = _you(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_SEARCH_BODY)) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + res = you.search(query="test") + assert len(w) == 0 + assert isinstance(res, SearchResponse) + + def test_unified_emits_deprecation_warning(self): + you = _you(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_SEARCH_BODY)) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + res = you.search.unified(query="test") + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "you.search()" in str(w[0].message) + assert isinstance(res, SearchResponse) + + @pytest.mark.asyncio + async def test_unified_async_emits_deprecation_warning(self): + async_you = You( + server_url="http://mock.local", + async_client=httpx.AsyncClient(transport=httpx.MockTransport(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_SEARCH_BODY))), + api_key_auth="test", + ) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + res = await async_you.search.unified_async(query="test") + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "you.search_async()" in str(w[0].message) + assert isinstance(res, SearchResponse) + + +class TestContentsShim: + def test_direct_call_no_warning(self): + you = _you(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY)) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + res = you.contents(urls=["https://example.com"]) + assert len(w) == 0 + assert len(res) == 1 + + def test_generate_emits_deprecation_warning(self): + you = _you(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY)) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + res = you.contents.generate(urls=["https://example.com"]) + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "you.contents()" in str(w[0].message) + assert len(res) == 1 + + @pytest.mark.asyncio + async def test_generate_async_emits_deprecation_warning(self): + async_you = You( + server_url="http://mock.local", + async_client=httpx.AsyncClient(transport=httpx.MockTransport(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY))), + api_key_auth="test", + ) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + res = await async_you.contents.generate_async(urls=["https://example.com"]) + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "you.contents_async()" in str(w[0].message) + assert len(res) == 1 diff --git a/tests/test_user_agent_hook.py b/tests/test_user_agent_hook.py deleted file mode 100644 index 768e73c..0000000 --- a/tests/test_user_agent_hook.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Unit tests for YDCUserAgentOverrideHook. - -This hook is a hand-maintained addition (not regenerated by Speakeasy). -The CHANGELOG flags it as regen-fragile: future SDK regens will overwrite -the file and the hook MUST be re-applied. These tests ensure a regen -revert is caught by CI, mirroring how tests/test_security_env.py guards -the env-var precedence hand-edit. -""" - -import httpx -from unittest.mock import Mock - -from youdotcom._hooks.registration import YDCUserAgentOverrideHook -from youdotcom._hooks.types import BeforeRequestContext, HookContext -from youdotcom._version import __user_agent__, __version__ - - -def _make_ctx(user_agent: str, sdk_version: str = __version__) -> BeforeRequestContext: - """Build a minimal BeforeRequestContext for hook testing.""" - config = Mock() - config.user_agent = user_agent - config.sdk_version = sdk_version - parent = HookContext( - config=config, - base_url="http://mock.local", - operation_id="test", - oauth2_scopes=None, - security_source=None, - tags=None, - extensions=None, - ) - return BeforeRequestContext(parent) - - -class TestYDCUserAgentOverrideHook: - def test_default_speakeasy_ua_is_overridden(self): - """When user_agent is the speakeasy default, the hook rewrites it to youdotcom-python-sdk/{version}.""" - hook = YDCUserAgentOverrideHook() - ctx = _make_ctx(__user_agent__) - request = httpx.Request("GET", "http://mock.local/test") - hook.before_request(ctx, request) - assert request.headers["User-Agent"] == f"youdotcom-python-sdk/{__version__}" - - def test_custom_user_agent_passes_through(self): - """When user_agent is a custom value (e.g. integration UA), the hook passes it through unchanged.""" - hook = YDCUserAgentOverrideHook() - custom_ua = "langchain-youdotcom/1.0" - ctx = _make_ctx(custom_ua) - request = httpx.Request("GET", "http://mock.local/test") - hook.before_request(ctx, request) - assert request.headers["User-Agent"] == custom_ua - - def test_speakeasy_prefix_ua_is_overridden(self): - """A UA starting with speakeasy-sdk/ (but different from default) is still overridden.""" - hook = YDCUserAgentOverrideHook() - ctx = _make_ctx("speakeasy-sdk/python 9.9.9 custom") - request = httpx.Request("GET", "http://mock.local/test") - hook.before_request(ctx, request) - assert request.headers["User-Agent"] == f"youdotcom-python-sdk/{__version__}" diff --git a/uv.lock b/uv.lock index 4307a77..ecb1231 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] @@ -30,16 +31,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + [[package]] name = "astroid" -version = "3.2.4" +version = "4.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/53/1067e1113ecaf58312357f2cd93063674924119d80d173adc3f6f2387aa2/astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a", size = 397576, upload-time = "2024-07-20T12:57:43.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, ] [[package]] @@ -83,7 +125,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -154,6 +196,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, ] +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/2f/ec5241c38e7fa0fe6c26bfc450e78b9489a6c3c08b394b85d2c10e506975/librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5", size = 148654, upload-time = "2026-07-08T12:24:30.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/d651e18d3ee7aa2879322368c4f278bb7ecaa6b90caadfdec4ebfa8389f3/librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547", size = 153537, upload-time = "2026-07-08T12:24:31.773Z" }, + { url = "https://files.pythonhosted.org/packages/45/18/10bff2122577246009d9619b6569596daf69b7648812f997ca9ca0426f60/librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2", size = 494336, upload-time = "2026-07-08T12:24:33.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/87dfee871b852970f137fdeae8e2ca356c5ab38e6f21d2a3299535fc3159/librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929", size = 485393, upload-time = "2026-07-08T12:24:34.324Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d5/625447a8c0441ff5f15f4ac5e1d323fb9d4d256ebfde7a3c8e003f646057/librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a", size = 515382, upload-time = "2026-07-08T12:24:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d8/1c8c49ea04235960426444deece9092a6b3a9587a850a81bae2335317411/librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac", size = 509483, upload-time = "2026-07-08T12:24:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/6f/65/f1760fc48050e215201a03506c32b7270159088d01f64557b53e39e74a45/librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7", size = 532503, upload-time = "2026-07-08T12:24:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/18/1b/793e281dcf494879eff99f642b63ebc9c7c58694a1c2d1e93362a22c7041/librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40", size = 537027, upload-time = "2026-07-08T12:24:39.34Z" }, + { url = "https://files.pythonhosted.org/packages/69/45/0801bbb40c9eea795d3dd3ce91c4c5f3fe7d42d23ec4be3e8cb283bcc754/librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a", size = 517100, upload-time = "2026-07-08T12:24:40.907Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6c/eb5f514f8e29d4924bc0ff4601dd7b4175557e182e7c0721e84cffa39b8a/librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde", size = 558653, upload-time = "2026-07-08T12:24:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/f140100d1b59fe87ff40b5ecbb4e27924335b189a784e230ee465452f6c2/librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8", size = 104402, upload-time = "2026-07-08T12:24:43.668Z" }, + { url = "https://files.pythonhosted.org/packages/22/7c/57e40fef7cfb61869341cb28bdcefe8a950bebcbecca74a397bae14dce4a/librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc", size = 125002, upload-time = "2026-07-08T12:24:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" }, + { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" }, + { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" }, + { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" }, + { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -165,40 +294,62 @@ wheels = [ [[package]] name = "mypy" -version = "1.15.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433, upload-time = "2025-02-05T03:49:29.145Z" }, - { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472, upload-time = "2025-02-05T03:49:16.986Z" }, - { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424, upload-time = "2025-02-05T03:49:46.908Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450, upload-time = "2025-02-05T03:50:05.89Z" }, - { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765, upload-time = "2025-02-05T03:49:33.56Z" }, - { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701, upload-time = "2025-02-05T03:49:38.981Z" }, - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload-time = "2025-02-05T03:50:28.25Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload-time = "2025-02-05T03:50:13.411Z" }, - { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload-time = "2025-02-05T03:50:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload-time = "2025-02-05T03:48:48.705Z" }, - { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload-time = "2025-02-05T03:49:03.628Z" }, - { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload-time = "2025-02-05T03:50:00.313Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" }, - { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" }, - { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/09/f2f5f45dae0c9a0891e4751a73312730e009395102e5d72a22a976cca41f/mypy-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc", size = 14927774, upload-time = "2026-07-13T11:28:38.224Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/345367effd3a6877275a94d481614bfca983f45e028c6290e2cc54603811/mypy-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3", size = 14000127, upload-time = "2026-07-13T11:30:19.57Z" }, + { url = "https://files.pythonhosted.org/packages/99/6c/a10b7a7b9f0a755fb94e27ae834d4cea9ad6c5221f9325eef8f182641feb/mypy-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805", size = 14229437, upload-time = "2026-07-13T11:28:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bd/a26a602acb1bbf849fa4bdac4bc657ee2f11c0c2a764a2cc87a5304e865c/mypy-2.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117", size = 15171457, upload-time = "2026-07-13T11:29:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/124f462bef69bcbc90b9358088460b6091954a3e004852fcd9948db617a5/mypy-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5", size = 15478281, upload-time = "2026-07-13T11:32:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/db/a4/8bdca6a8ac8d856d82ed049144af2721245a135c2e8001d3890c93975852/mypy-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494", size = 11148008, upload-time = "2026-07-13T11:34:17.332Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/490eea348e60ba50decec20bc750605444149a5d7a8cc560042f90ba2c75/mypy-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee", size = 10142329, upload-time = "2026-07-13T11:32:52.116Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725, upload-time = "2026-07-13T11:32:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128, upload-time = "2026-07-13T11:32:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742, upload-time = "2026-07-13T11:33:03.313Z" }, + { url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418, upload-time = "2026-07-13T11:31:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164, upload-time = "2026-07-13T11:31:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472, upload-time = "2026-07-13T11:27:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800, upload-time = "2026-07-13T11:30:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, ] [[package]] @@ -228,6 +379,15 @@ 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 = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "platformdirs" version = "4.5.1" @@ -390,7 +550,7 @@ wheels = [ [[package]] name = "pylint" -version = "3.2.3" +version = "4.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, @@ -402,9 +562,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/e9/60280b14cc1012794120345ce378504cf17409e38cd88f455dc24e0ad6b5/pylint-3.2.3.tar.gz", hash = "sha256:02f6c562b215582386068d52a30f520d84fdbcf2a95fc7e855b816060d048b60", size = 1506739, upload-time = "2024-06-06T14:19:17.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/1d/3bb57f303701549550d74bf7ced2b07412be97125c167a0c9d216aa9f762/pylint-4.0.6.tar.gz", hash = "sha256:52f19191bee08bf103f9705ad1a0ece4aa5a0a4ef2bdcbd969375a1e6f6579d5", size = 1585588, upload-time = "2026-06-14T14:43:26.772Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/d3/d346f779cbc9384d8b805a7557b5f2b8ee9f842bffebec9fc6364d6ae183/pylint-3.2.3-py3-none-any.whl", hash = "sha256:b3d7d2708a3e04b4679e02d99e72329a8b7ee8afb8d04110682278781f889fa8", size = 519244, upload-time = "2024-06-06T14:19:13.228Z" }, + { url = "https://files.pythonhosted.org/packages/ab/da/acb2e7d4dbd2dfb792d38c0d850481f29ad7049b356d23f56c687d35203b/pylint-4.0.6-py3-none-any.whl", hash = "sha256:d11a0e1fdb7b1cd46ec5d6fc78fee8b95f28695b2d6140e5809925f61e32ea54", size = 538389, upload-time = "2026-06-14T14:43:24.873Z" }, ] [[package]] @@ -533,7 +693,7 @@ wheels = [ [[package]] name = "youdotcom" -version = "2.5.0" +version = "2.6.0" source = { editable = "." } dependencies = [ { name = "httpcore" }, @@ -559,9 +719,9 @@ requires-dist = [ [package.metadata.requires-dev] 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" }, + { name = "mypy", specifier = ">=2.3.0" }, + { name = "pylint", specifier = ">=4.0.0" }, + { name = "pyright", specifier = ">=1.1.398" }, + { name = "pytest", specifier = ">=9.0.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, ] From 0d038065b9dac0a9ba66de5e5dc43c840aa84182 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 15:42:02 -0700 Subject: [PATCH 02/20] Address review feedback: fix MIGRATION anchor, CONTRIBUTING test cmd, empty api_key_auth opt-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix broken MIGRATION.md anchor link (#250-to-260 → #250--260) - CONTRIBUTING.md: add --ignore=tests/test_performance.py to unit test command - Security: explicit empty api_key_auth now constructs Security(api_key_auth=None) instead of falling back to env vars (prevents confused-deputy footgun) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- MIGRATION.md | 2 +- src/youdotcom/sdk.py | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b376021..fa339fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ If you encounter any bugs or have suggestions for improvements, please open an i 1. Fork the repository and create a branch from `main`. 2. Make your changes. Follow existing code style and patterns. -3. Add or update tests as needed. Run `pytest tests/ --ignore=tests/test_live.py` for unit tests (live tests require `YDC_API_KEY`). +3. Add or update tests as needed. Run `pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py` for unit tests (live tests require `YDC_API_KEY`). 4. Run `mypy src/youdotcom/` to ensure type safety. 5. Update documentation (README, CHANGELOG, `docs/` directory) if your change adds or modifies public API surface. 6. Open a pull request with a clear description of the change. diff --git a/MIGRATION.md b/MIGRATION.md index 2d599fd..b37de3d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -437,7 +437,7 @@ res = you.contents.generate(urls=["https://example.com"], crawl_timeout=5) ## 1.x to 2.0 -> **Note:** If you are upgrading to 2.6.0+, the Agents API (`you.agents.runs.create()`) and all agent model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, `ResponseCreated`, `ResponseStarting`, `ResponseOutputTextDelta`, `ResponseOutputContentFull`, `ResponseDone`, `ReportVerbosity`, `SearchEffort`, `AgentRuns401ResponseError`, etc.) have been removed entirely. The migration steps below that reference agent imports and calls are no longer valid — see the [2.5.0 to 2.6.0](#250-to-260) section above. The Search and Contents API changes below still apply. +> **Note:** If you are upgrading to 2.6.0+, the Agents API (`you.agents.runs.create()`) and all agent model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, `ResponseCreated`, `ResponseStarting`, `ResponseOutputTextDelta`, `ResponseOutputContentFull`, `ResponseDone`, `ReportVerbosity`, `SearchEffort`, `AgentRuns401ResponseError`, etc.) have been removed entirely. The migration steps below that reference agent imports and calls are no longer valid — see the [2.5.0 to 2.6.0](#250--260) section above. The Search and Contents API changes below still apply. This guide helps you upgrade your code from You.com Python SDK 1.x to 2.0. diff --git a/src/youdotcom/sdk.py b/src/youdotcom/sdk.py index d9f010c..fbf837e 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -87,15 +87,17 @@ def __init__( ), "The provided async_client must implement the AsyncHttpClient protocol." security: Any = None - # Normalize empty strings to None so an empty X-API-Key header - # isn't sent (e.g. You(api_key_auth=os.getenv("YDC_API_KEY", ""))) + # An explicit empty string (e.g. You(api_key_auth=os.getenv("YDC_API_KEY", ""))) + # is treated as an opt-out: we construct Security with api_key_auth=None so no + # X-API-Key header is sent AND we don't fall back to env vars. Only api_key_auth=None + # (the default) allows env fallback via get_security_from_env. if callable(api_key_auth): # pylint: disable=unnecessary-lambda-assignment security = lambda: models.Security( api_key_auth=(api_key_auth() or None) ) - elif api_key_auth: - security = models.Security(api_key_auth=api_key_auth) + elif api_key_auth is not None: + security = models.Security(api_key_auth=(api_key_auth or None)) else: security = None From fbc37fe1a100bc4a328c1c91d2747d21861e51f0 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 16:09:09 -0700 Subject: [PATCH 03/20] Clean up: delete stale generate-sdk skill, fix overlay comment in test_live.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete .agents/skills/generate-sdk-and-open-pr/SKILL.md — was a Speakeasy generation workflow, no longer relevant since SDK is hand-maintained - Remove empty .agents/ directory tree - Fix stale 'overlay-generated' comment in test_live.py Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../skills/generate-sdk-and-open-pr/SKILL.md | 161 ------------------ tests/test_live.py | 2 +- 2 files changed, 1 insertion(+), 162 deletions(-) delete mode 100644 .agents/skills/generate-sdk-and-open-pr/SKILL.md diff --git a/.agents/skills/generate-sdk-and-open-pr/SKILL.md b/.agents/skills/generate-sdk-and-open-pr/SKILL.md deleted file mode 100644 index 0dd146a..0000000 --- a/.agents/skills/generate-sdk-and-open-pr/SKILL.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -name: generate-sdk-and-open-pr -description: Release a new version of the youdotcom Python SDK and open a PR -allowed-tools: Bash, Read, Write, Edit, Glob, Grep -metadata: - author: youdotcom-oss - version: "2.0.0" - category: release - keywords: release, version, publish, pypi ---- - -# Release - -Release a new version of the `youdotcom` Python SDK to PyPI and GitHub. - -The SDK is hand-maintained (no longer generated by Speakeasy). This skill -covers version bumping, changelog updates, testing, and opening a release PR. - -## Step 1: Check current versions - -```bash -git fetch --all --tags -gh release list --repo youdotcom-oss/youdotcom-python-sdk --limit 1 -curl -s https://pypi.org/pypi/youdotcom/json | python3 -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" -``` - -Read the version from `pyproject.toml` (line 3) and `src/youdotcom/_version.py`. - -Present a summary: -- GitHub release: latest tag -- PyPI version: latest published -- Local version: in pyproject.toml and _version.py -- Unreleased commits: `git log ..HEAD --oneline` - -## Step 2: Confirm the next version - -Analyze unreleased commits to determine the semver bump: -- **patch** (X.Y.Z+1): bug fixes, dependency updates, docs -- **minor** (X.Y+1.0): new features, non-breaking additions -- **major** (X+1.0.0): breaking API changes, removed endpoints - -Use `AskUserQuestion` to confirm the version with the user. Do NOT proceed until confirmed. - -## Step 3: Create a release branch - -```bash -git checkout -b release/X.Y.Z -``` - -## Step 4: Update version - -Update the version string in: -- `pyproject.toml` — `version = "X.Y.Z"` -- `src/youdotcom/_version.py` — `__version__: str = "X.Y.Z"` (`__user_agent__` is derived automatically) - -## Step 5: Update documentation - -### CHANGELOG.md - -Add a new section at the top, following Keep a Changelog format: - -```markdown -## [X.Y.Z] - YYYY-MM-DD - -### Added -- ... - -### Changed -- ... - -### Removed -- ... -``` - -Analyze `git diff ..HEAD` to determine what changed. - -### MIGRATION.md - -Only update if there are breaking changes. Add a migration section with before/after code examples. - -### README.md - -Update the error class count, method count, and any examples if the API surface changed. - -## Step 6: Run tests - -### Start the mock server - -```bash -cd tests/mockserver && go build -o mockserver . && ./mockserver & -sleep 1 -cd ../.. -``` - -### Unit tests - -```bash -.venv/bin/python -m pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v -``` - -### Performance tests - -```bash -.venv/bin/python -m pytest tests/test_performance.py -v -``` - -### Type checking - -```bash -.venv/bin/mypy src/youdotcom/ -``` - -### Live tests (if API key available) - -```bash -YDC_API_KEY=... .venv/bin/python -m pytest tests/test_live.py -v -``` - -### Stop the mock server - -```bash -kill $(lsof -ti:18080) -``` - -All tests and mypy must pass before committing. - -## Step 7: Verify server URLs - -```bash -# Default server (answer, research, finance_research) = api.you.com -grep "api.you.com" src/youdotcom/sdkconfiguration.py -# Search/contents servers = ydc-index.io -grep "ydc-index.io" src/youdotcom/models/searchop.py src/youdotcom/models/contentsop.py -``` - -Search and contents methods use `SEARCH_OP_SERVERS[0]` / `CONTENTS_OP_SERVERS[0]` explicitly. Answer, research, and finance_research use the default `SERVERS[0]`. - -## Step 8: Verify hand-maintained files - -These files are hand-maintained and must not be lost: -- `src/youdotcom/_shims.py` — backward-compat shims (`SearchShim`, `ContentsShim`) -- `src/youdotcom/research_helpers.py` — background-mode research helpers -- `src/youdotcom/errors/paymentrequired_response_error.py` — 402 error class - -## Step 9: Commit, push, and open PR - -```bash -git add -A -git commit -m "Release vX.Y.Z: " -git push -u origin release/X.Y.Z -``` - -Open a PR against `main`: - -```bash -gh pr create --base main --head release/X.Y.Z \ - --title "Release vX.Y.Z: " \ - --body "" -``` - -Report the PR URL to the user. diff --git a/tests/test_live.py b/tests/test_live.py index 3781cd9..88658d2 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -288,7 +288,7 @@ def test_research_with_sources(self, you_client): class TestLiveResearchOutputSchema: """Live test for Research `output_schema` parameter (beta feature). - Smoke-tests prod to ensure the overlay-generated + Smoke-tests prod to ensure the `Content = Union[str, Dict[str, Any]]` round-trips structured payloads. """ From c892ed306a374b2d58f932d3f65c48043fee3ca9 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 16:10:03 -0700 Subject: [PATCH 04/20] Fix MIGRATION: remove misleading 'reverted to keyed' language Keyless search never shipped in a release, so referencing a revert is misleading. Renamed heading and header to describe the current state without implying users experienced keyless. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- MIGRATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index b37de3d..f0dbd71 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2,7 +2,7 @@ ## 2.5.0 → 2.6.0 -> **This release adds new direct methods, removes the Agents API, and reverts search to keyed `POST /v1/search`.** The old sub-SDK patterns still work but emit `DeprecationWarning`. Migrate at your convenience. +> **This release adds new direct methods, removes the Agents API, and moves search to `POST /v1/search`.** The old sub-SDK patterns still work but emit `DeprecationWarning`. Migrate at your convenience. ### Answer API @@ -23,7 +23,7 @@ with You(api_key_auth=os.getenv("YDC_API_KEY")) as you: Requires an API key. `country` and `language` accept plain strings (e.g. `"us"`, `"en"`) and are normalized to uppercase. -### Search Reverted to Keyed POST /v1/search +### Search Moved to Direct Method `you.search()` / `you.search_async()` target `POST /v1/search` on `ydc-index.io`. Search requires an API key. From 4e7f7e5846925d8b2c97f69b36cf1f8ef28c6481 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 16:12:10 -0700 Subject: [PATCH 05/20] Remove stale Speakeasy/overlay references from active files - Remove 'Hand-maintained additions' section from CHANGELOG 2.4.0 (references deleted registration.py, removed YDCUserAgentOverrideHook, and Speakeasy regen workflow that no longer exists) - Remove .speakeasy/ entries from .gitignore - Fix stale 'overlay' references in examples/api-example-calls.py, MIGRATION.md, tests/test_research.py - Fix stale 'reverted overlay' comment in tests/test_research.py - Fix stale 'Speakeasy regen' comment in tests/test_security_env.py - Clean up Speakeasy reference in CHANGELOG 2.4.0 Fixed section Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .gitignore | 3 --- CHANGELOG.md | 13 +------------ MIGRATION.md | 5 ++--- examples/api-example-calls.py | 5 ++--- tests/test_research.py | 12 +++++------- tests/test_security_env.py | 4 ++-- 6 files changed, 12 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 9db7a30..1a8e80a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,6 @@ src/*.egg-info/ .python-version .DS_Store pyrightconfig.json -**/.speakeasy/temp/ -**/.speakeasy/logs/ -.speakeasy/reports .env .env.local _debug/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b06cde..33b2510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,18 +151,7 @@ export YDC_API_KEY="your-api-key" ### Fixed -- **`output_schema` requests no longer send an empty `{}` body**: the Research request body declares `output_schema` as an inline `type: object` schema (no `$ref`, no `properties`). Speakeasy was emitting `class OutputSchema(BaseModel): r"""..."""` — a docstring-only body — so pydantic's `extra="ignore"` stripped every JSON Schema field on serialize, leaving the server to receive `{}` and return 422 (`Structured output schema root must be an object`). The overlay now injects `additionalProperties: true` on the inline schema (mirror of the response-side `Content` fix), so `OutputSchema` round-trips as `Optional[Dict[str, Any]]` and the JSON Schema reaches the server intact. Regression caught before release by `tests/test_live.py::TestLiveResearchOutputSchema::test_research_output_schema_structured_payload` against prod. - -### Hand-maintained additions - -Two files in the SDK are hand-maintained rather than fully regenerated each cycle: - -- **`src/youdotcom/utils/security.py`** — generated with a `Code generated ... DO NOT EDIT` header (Speakeasy will overwrite on regen). The hand-edit below MUST be re-applied after every regen (or moved into the Speakeasy overlay / `x-speakeasy-env-var` extension). -- **`src/youdotcom/_hooks/registration.py`** — generated-once per its file header (`This file is only ever generated once on the first generation and then is free to be modified`); Speakeasy does **not** overwrite it on subsequent regens, so the hand-edit below is regen-safe and will survive. - - - **`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 precedence chain after regen, or move it into the Speakeasy overlay. - - - **`YDCUserAgentOverrideHook` honors custom `user_agent`** (regen-safe; lives in `registration.py`): 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 prefix (`speakeasy-sdk/`) 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. +- **`output_schema` requests no longer send an empty `{}` body**: the Research request body declares `output_schema` as an inline `type: object` schema (no `$ref`, no `properties`). Previously the body was a docstring-only model, so pydantic's `extra="ignore"` stripped every JSON Schema field on serialize, leaving the server to receive `{}` and return 422 (`Structured output schema root must be an object`). Now `OutputSchema` round-trips as `Optional[Dict[str, Any]]` and the JSON Schema reaches the server intact. Regression caught before release by `tests/test_live.py::TestLiveResearchOutputSchema::test_research_output_schema_structured_payload` against prod. --- diff --git a/MIGRATION.md b/MIGRATION.md index f0dbd71..9b819ef 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -327,9 +327,8 @@ res = you.research( }, ) assert res.output.content_type.value == "object" -# Content is now Union[str, Dict[str, Any]] — the overlay injects -# additionalProperties: true so the structured payload round-trips -# as a plain dict. +# Content is now Union[str, Dict[str, Any]] — when content_type is +# "object" the structured payload round-trips as a plain dict. print(res.output.content) # {'same_entity': True, 'confidence': 0.95, 'evidence': [...]} print(res.output.content["same_entity"]) # True diff --git a/examples/api-example-calls.py b/examples/api-example-calls.py index 73f2522..64642bb 100755 --- a/examples/api-example-calls.py +++ b/examples/api-example-calls.py @@ -277,9 +277,8 @@ def research_output_schema_request(): assert isinstance(res, ResearchResponse) print(f"content_type: {res.output.content_type.value}") - # output.content is Union[str, Dict[str, Any]] via the overlay's - # additionalProperties: true. When content_type is "object" it is a - # plain dict, so index it directly. + # output.content is Union[str, Dict[str, Any]]. When content_type is + # "object" it is a plain dict, so index it directly. structured_content = res.output.content print(f"structured payload: {structured_content}") diff --git a/tests/test_research.py b/tests/test_research.py index a3318d3..dfa1deb 100644 --- a/tests/test_research.py +++ b/tests/test_research.py @@ -389,8 +389,7 @@ class TestResearchOutputSchema: 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 both the ``content_type`` slot and the - structured payload (preserved via ``additionalProperties: true`` in the - overlay, which makes ``Content`` a ``Union[str, Dict[str, Any]]``). + structured payload (``Content`` is ``Union[str, Dict[str, Any]]``). """ def test_output_schema_sets_content_type_to_object(self, server_url, api_key): @@ -403,8 +402,8 @@ def test_output_schema_sets_content_type_to_object(self, server_url, api_key): def handler(request): body = json.loads(request.content) assert "output_schema" in body - # Guard the 2.4.0 regression: a reverted overlay serializes - # output_schema to an empty {}. Assert the schema fields survive. + # Guard the 2.4.0 regression: output_schema serializes + # to an empty {}. Assert the schema fields survive. assert body["output_schema"].get("properties", {}).get("same_entity") assert "same_entity" in body["output_schema"].get("required", []) return httpx.Response( @@ -444,9 +443,8 @@ def handler(request): assert isinstance(res, ResearchResponse) assert res.output.content_type.value == "object" - # Content is now Union[str, Dict[str, Any]] — the overlay injected - # additionalProperties: true so the structured payload round-trips - # as a plain dict. + # Content is now Union[str, Dict[str, Any]] — when content_type + # is "object" the structured payload round-trips as a plain dict. assert res.output.content is not None assert res.output.content == structured_payload assert res.output.content["same_entity"] is True diff --git a/tests/test_security_env.py b/tests/test_security_env.py index a56cb22..d40b0ee 100644 --- a/tests/test_security_env.py +++ b/tests/test_security_env.py @@ -2,8 +2,8 @@ `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. +These tests lock in the precedence so accidental regressions are caught +by CI immediately. """ import os From 730ce7d36abb9a85c2be2c2e0d0b4fc2f45bfe37 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 16:12:36 -0700 Subject: [PATCH 06/20] Fix MIGRATION header: search endpoint didn't change, only the method The POST /v1/search endpoint was already supported in 2.5.x. The actual change is search_helpers.search() became you.search(). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 9b819ef..5431698 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2,7 +2,7 @@ ## 2.5.0 → 2.6.0 -> **This release adds new direct methods, removes the Agents API, and moves search to `POST /v1/search`.** The old sub-SDK patterns still work but emit `DeprecationWarning`. Migrate at your convenience. +> **This release adds the Answer API, removes the Agents API, and makes `search()` a direct method on `You`.** The old sub-SDK patterns still work but emit `DeprecationWarning`. Migrate at your convenience. ### Answer API From 19cd6f6827a8ecbf5247aa97503ff8ae9a488c6b Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 16:42:21 -0700 Subject: [PATCH 07/20] Address review feedback: dataclass field, pip install, pytest-xdist, example paths - Fix SDKConfiguration: use dataclasses.field instead of pydantic.Field (stdlib @dataclass doesn't interpret pydantic.Field) - Fix tests/README.md: replace broken 'pip install -e ".[dev"]' with 'pip install -e . mypy pylint pyright pytest pytest-asyncio' - Fix PERFORMANCE_TESTING.md: document pytest-xdist requirement for -n auto - Fix examples/api-example-calls.py: venv name (.venv not venv) and run path (python examples/api-example-calls.py) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- examples/api-example-calls.py | 6 +++--- src/youdotcom/sdkconfiguration.py | 5 ++--- tests/PERFORMANCE_TESTING.md | 3 +++ tests/README.md | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/api-example-calls.py b/examples/api-example-calls.py index 64642bb..cd08e37 100755 --- a/examples/api-example-calls.py +++ b/examples/api-example-calls.py @@ -6,14 +6,14 @@ Setup Instructions: ------------------- 1. Create and activate a virtual environment: - python3 -m venv venv + python3 -m venv .venv source .venv/bin/activate # On Windows: .venv\\Scripts\\activate 2. Install the package: pip install youdotcom -3. Run the script: - python api-example-calls.py +3. Run the script from the repo root: + python examples/api-example-calls.py The script will prompt you to enter your API key at runtime. """ diff --git a/src/youdotcom/sdkconfiguration.py b/src/youdotcom/sdkconfiguration.py index ec05e3e..049fba3 100644 --- a/src/youdotcom/sdkconfiguration.py +++ b/src/youdotcom/sdkconfiguration.py @@ -7,8 +7,7 @@ ) from .httpclient import AsyncHttpClient, HttpClient from .utils import Logger, RetryConfig, remove_suffix -from dataclasses import dataclass -from pydantic import Field +from dataclasses import dataclass, field from typing import Callable, Dict, Optional, Tuple, Union from youdotcom import models from youdotcom.types import OptionalNullable, UNSET @@ -34,7 +33,7 @@ class SDKConfiguration: openapi_doc_version: str = __openapi_doc_version__ sdk_version: str = __version__ user_agent: str = __user_agent__ - retry_config: OptionalNullable[RetryConfig] = Field(default_factory=lambda: UNSET) + retry_config: OptionalNullable[RetryConfig] = field(default_factory=lambda: UNSET) timeout_ms: Optional[int] = None def get_server_details(self) -> Tuple[str, Dict[str, str]]: diff --git a/tests/PERFORMANCE_TESTING.md b/tests/PERFORMANCE_TESTING.md index 01d9fac..4823b38 100644 --- a/tests/PERFORMANCE_TESTING.md +++ b/tests/PERFORMANCE_TESTING.md @@ -256,6 +256,9 @@ pytest tests/test_performance.py -v ### Parallel Execution ```bash +# Install pytest-xdist first (not included in dev deps): +pip install pytest-xdist + # Run tests in parallel (faster, but results may vary) pytest tests/test_performance.py -n auto -v ``` diff --git a/tests/README.md b/tests/README.md index 6d17868..0e5ba8d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -40,8 +40,8 @@ go run . ```bash # Install dependencies first uv sync --dev -# or -pip install -e ".[dev]" +# or with pip: +pip install -e . mypy pylint pyright pytest pytest-asyncio # Run tests pytest tests/ -v @@ -161,7 +161,7 @@ These tests are designed to run in CI/CD environments. The automated script ensu ## Troubleshooting -**Tests not found**: Ensure you've installed dev dependencies with `uv sync --dev` or `pip install -e ".[dev]"` +**Tests not found**: Ensure you've installed dev dependencies with `uv sync --dev` or `pip install -e . mypy pylint pyright pytest pytest-asyncio` **Mock server fails to start**: Ensure you have either Go (1.21+) or Docker installed From 49ee6d696c6184dad39ab223946af61eaba35fbf Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 17:11:43 -0700 Subject: [PATCH 08/20] Fix string comparison in _populate_from_globals: use != instead of is not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'is not' is identity comparison, not equality — can silently fail for strings depending on interning, causing globals-based overrides to be skipped. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/youdotcom/utils/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/youdotcom/utils/values.py b/src/youdotcom/utils/values.py index 6e25f4b..3be1346 100644 --- a/src/youdotcom/utils/values.py +++ b/src/youdotcom/utils/values.py @@ -91,7 +91,7 @@ def _populate_from_globals( found = False for name in global_fields: field = global_fields[name] - if name is not param_name: + if name != param_name: continue found = True From 82f3ead52d2eb7e25e2ee1a1fa37ba6c37e2d00c Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 17:44:56 -0700 Subject: [PATCH 09/20] CI: exclude performance tests from test workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same fix as CONTRIBUTING.md — performance tests need timing infrastructure and are slow, not suitable for CI unit test run. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ca03e7..1767107 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: curl -sf http://localhost:18080/ || echo "mock server ready" - name: Run tests - run: pytest tests/ -v --tb=short -x --ignore=tests/test_live.py + run: pytest tests/ -v --tb=short -x --ignore=tests/test_live.py --ignore=tests/test_performance.py - name: Run mypy run: mypy src/youdotcom/ From f3be297635287fd5966d240c7e2a0b68922805b7 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 17:48:17 -0700 Subject: [PATCH 10/20] CI: fix mock server health check and remove -x flag - Hit /_mockserver/health (returns 200) with retry loop instead of broken 'curl -sf / || echo ready' that always printed ready - Remove -x flag so CI shows all failures, not just the first one Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1767107..6f70d42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,11 +36,17 @@ jobs: run: | go build -o mockserver . ./mockserver & - sleep 2 - curl -sf http://localhost:18080/ || echo "mock server ready" + for i in $(seq 1 10); do + if curl -sf http://localhost:18080/_mockserver/health; then + echo "mock server ready" + break + fi + echo "waiting for mock server..." + sleep 1 + done - name: Run tests - run: pytest tests/ -v --tb=short -x --ignore=tests/test_live.py --ignore=tests/test_performance.py + run: pytest tests/ -v --tb=short --ignore=tests/test_live.py --ignore=tests/test_performance.py - name: Run mypy run: mypy src/youdotcom/ From 97d6d8d80196ed2b8f7a92d756b8b2ba796121bb Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 17:54:01 -0700 Subject: [PATCH 11/20] Add SDK drift check against You.com OpenAPI specs - scripts/check_drift.py: fetches OpenAPI specs from you.com and compares endpoints, server URLs, and enum values against the SDK surface - .github/workflows/test.yml: non-blocking drift check on every PR - .github/workflows/drift-check.yml: weekly scheduled check that opens a GitHub issue if drift is detected, closes stale issues when clean Known uncovered APIs (billing, images) and endpoints (GET /v1/search) are excluded via exception lists. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/drift-check.yml | 58 +++++++ .github/workflows/test.yml | 19 ++ scripts/check_drift.py | 278 ++++++++++++++++++++++++++++++ 3 files changed, 355 insertions(+) create mode 100644 .github/workflows/drift-check.yml create mode 100644 scripts/check_drift.py diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml new file mode 100644 index 0000000..92be285 --- /dev/null +++ b/.github/workflows/drift-check.yml @@ -0,0 +1,58 @@ +name: drift-check + +on: + schedule: + # Every Monday at 9:00 UTC + - cron: "0 9 * * 1" + workflow_dispatch: + +jobs: + drift-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install SDK + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Run drift check + id: drift + run: python scripts/check_drift.py --strict --verbose + continue-on-error: true + + - name: Open issue on drift + if: steps.drift.outcome == 'failure' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Create drift label if it doesn't exist + gh label create drift --color FBCA04 --description "SDK drift from OpenAPI specs" 2>/dev/null || true + # Re-run to capture output for the issue body + OUTPUT=$(python scripts/check_drift.py --strict --verbose 2>&1 || true) + gh issue create \ + --title "SDK drift detected: OpenAPI specs vs SDK surface" \ + --body "The weekly drift check found differences between the You.com OpenAPI specs and the SDK. + + \`\`\` + ${OUTPUT} + \`\`\` + + Review the warnings above and update the SDK as needed. Run \`python scripts/check_drift.py --verbose\` locally to reproduce." \ + --label "drift" + + - name: Close stale drift issues + if: steps.drift.outcome == 'success' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Close any open drift issues since no drift was detected + gh issue list --label "drift" --state open --json number --jq '.[].number' | while read -r num; do + gh issue close "$num" --comment "No drift detected in the latest check. Closing." + done diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f70d42..f986137 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,3 +50,22 @@ jobs: - name: Run mypy run: mypy src/youdotcom/ + + drift-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install SDK + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Run drift check (non-blocking) + run: python scripts/check_drift.py --verbose + continue-on-error: true diff --git a/scripts/check_drift.py b/scripts/check_drift.py new file mode 100644 index 0000000..51cbcc1 --- /dev/null +++ b/scripts/check_drift.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +""" +Drift check: compares the You.com Python SDK against the official OpenAPI specs. + +Fetches OpenAPI specs from you.com/docs/openapi/ and compares: + 1. Endpoints — every path+method in the specs has a corresponding SDK method + 2. Server URLs — spec servers match SDK server constants + 3. Enums — spec enum values match SDK enum classes + 4. New APIs — specs the SDK doesn't cover yet (except known exceptions) + +Usage: + python scripts/check_drift.py # Print warnings, exit 0 (CI, non-blocking) + python scripts/check_drift.py --strict # Exit 1 on drift (scheduled workflow) + python scripts/check_drift.py --verbose # Show all checks, even passing ones +""" + +import argparse +import sys +from typing import Any + +import httpx + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +OPENAPI_INDEX = "https://you.com/docs/openapi.json" +OPENAPI_BASE = "https://you.com/docs/openapi/" + +# APIs the SDK intentionally doesn't cover yet. +KNOWN_UNCOVERED = {"billing", "images"} + +# Endpoints the SDK intentionally doesn't support (e.g. legacy GET search). +KNOWN_UNCOVERED_ENDPOINTS = { + ("GET", "/v1/search"), +} + +# Map (method, path) -> SDK method name. +# {task_id} and {task_id}/stream are handled by helpers, not direct methods. +EXPECTED_ENDPOINTS = { + ("POST", "/v1/search"): "you.search()", + ("POST", "/v1/contents"): "you.contents()", + ("POST", "/v1/answer"): "you.answer()", + ("POST", "/v1/research"): "you.research()", + ("GET", "/v1/research/{task_id}"): "you.get_research_task()", + ("GET", "/v1/research/{task_id}/stream"): "stream_research() (helper)", + ("POST", "/v1/finance_research"): "you.finance_research()", +} + +# Map spec name -> SDK server URL constant. +EXPECTED_SERVERS = { + "web-search": ("SEARCH_OP_SERVERS", "https://ydc-index.io"), + "contents": ("CONTENTS_OP_SERVERS", "https://ydc-index.io"), + "answer": ("SERVERS (default)", "https://api.you.com"), + "research": ("SERVERS (default)", "https://api.you.com"), + "finance-research": ("SERVERS (default)", "https://api.you.com"), +} + +# Map (spec name, schema name fragment) -> SDK enum class import. +# We match schema names that contain the fragment. +ENUM_CHECKS = [ + ("research", "ResearchEffort", "youdotcom.models", "ResearchEffort"), + ("finance-research", "ResearchEffort", "youdotcom.models", "FinanceResearchEffort"), + ("web-search", "Freshness", "youdotcom.models", "Freshness"), + ("answer", "Freshness", "youdotcom.models", "Freshness"), + ("web-search", "Country", "youdotcom.models", "Country"), + ("answer", "Country", "youdotcom.models", "Country"), + ("research", "Country", "youdotcom.models", "Country"), +] + + +# --------------------------------------------------------------------------- +# Spec fetching +# --------------------------------------------------------------------------- + +def fetch_specs() -> dict[str, dict[str, Any]]: + """Fetch all OpenAPI specs from the You.com docs index.""" + client = httpx.Client(timeout=30, follow_redirects=True) + r = client.get(OPENAPI_INDEX) + r.raise_for_status() + + # The index page is HTML with relative links like: + # Web Search + import re + matches = re.findall(r'href="openapi/([\w-]+)\.json"', r.text) + if not matches: + raise RuntimeError("Could not find any OpenAPI spec links in the index page") + + specs: dict[str, dict[str, Any]] = {} + for name in matches: + url = f"{OPENAPI_BASE}{name}.json" + resp = client.get(url) + resp.raise_for_status() + specs[name] = resp.json() + + client.close() + return specs + + +# --------------------------------------------------------------------------- +# Drift checks +# --------------------------------------------------------------------------- + +def check_endpoints(specs: dict[str, dict[str, Any]]) -> list[str]: + """Check that every path+method in specs has a corresponding SDK method.""" + warnings: list[str] = [] + + for spec_name, spec in specs.items(): + if spec_name in KNOWN_UNCOVERED: + continue + + for path, path_item in spec.get("paths", {}).items(): + for method in path_item: + if method.upper() not in ("GET", "POST", "PUT", "DELETE", "PATCH"): + continue + key = (method.upper(), path) + if key in KNOWN_UNCOVERED_ENDPOINTS: + continue + if key not in EXPECTED_ENDPOINTS: + warnings.append( + f"[endpoint] {spec_name}: {method.upper()} {path} " + f"is in the OpenAPI spec but has no SDK method" + ) + + # Also check for SDK endpoints that are no longer in any spec + all_spec_endpoints: set[tuple[str, str]] = set() + for spec_name, spec in specs.items(): + if spec_name in KNOWN_UNCOVERED: + continue + for path, path_item in spec.get("paths", {}).items(): + for method in path_item: + if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH"): + all_spec_endpoints.add((method.upper(), path)) + + for endpoint, sdk_method in EXPECTED_ENDPOINTS.items(): + if endpoint not in all_spec_endpoints: + warnings.append( + f"[endpoint] SDK has {sdk_method} for {endpoint[0]} {endpoint[1]} " + f"but it's not in any OpenAPI spec (may have been removed)" + ) + + return warnings + + +def check_server_urls(specs: dict[str, dict[str, Any]]) -> list[str]: + """Check that spec server URLs match SDK server constants.""" + warnings: list[str] = [] + + for spec_name, spec in specs.items(): + if spec_name in KNOWN_UNCOVERED: + continue + if spec_name not in EXPECTED_SERVERS: + continue + + sdk_const, expected_url = EXPECTED_SERVERS[spec_name] + spec_servers = [s["url"] for s in spec.get("servers", [])] + + if expected_url not in spec_servers: + warnings.append( + f"[server] {spec_name}: spec servers={spec_servers} " + f"but SDK expects {expected_url} ({sdk_const})" + ) + + return warnings + + +def check_enums(specs: dict[str, dict[str, Any]]) -> list[str]: + """Check that spec enum values match SDK enum classes.""" + warnings: list[str] = [] + + # Import SDK enum classes + import importlib + sdk_enums: dict[str, Any] = {} + for _, _, module_name, class_name in ENUM_CHECKS: + if class_name not in sdk_enums: + mod = importlib.import_module(module_name) + sdk_enums[class_name] = getattr(mod, class_name) + + for spec_name, spec in specs.items(): + if spec_name in KNOWN_UNCOVERED: + continue + + schemas = spec.get("components", {}).get("schemas", {}) + for schema_name, schema in schemas.items(): + # Only check string enums + if schema.get("type") != "string" or "enum" not in schema: + continue + + spec_values = set(schema["enum"]) + + # Find matching SDK enum class + for check_spec, name_fragment, _, sdk_class_name in ENUM_CHECKS: + if check_spec != spec_name: + continue + if name_fragment.lower() not in schema_name.lower(): + continue + + sdk_class = sdk_enums.get(sdk_class_name) + if sdk_class is None: + continue + + sdk_values = {e.value for e in sdk_class} + + if spec_values != sdk_values: + missing_in_sdk = spec_values - sdk_values + missing_in_spec = sdk_values - spec_values + if missing_in_sdk: + warnings.append( + f"[enum] {sdk_class_name}: spec has {missing_in_sdk} " + f"which SDK doesn't (schema: {schema_name})" + ) + if missing_in_spec: + warnings.append( + f"[enum] {sdk_class_name}: SDK has {missing_in_spec} " + f"which spec doesn't (schema: {schema_name}) — possible fabricated value" + ) + + return warnings + + +def check_new_apis(specs: dict[str, dict[str, Any]]) -> list[str]: + """Check for API specs the SDK doesn't cover (excluding known exceptions).""" + warnings: list[str] = [] + + for spec_name in specs: + if spec_name in KNOWN_UNCOVERED: + continue + if spec_name not in EXPECTED_SERVERS: + warnings.append( + f"[coverage] {spec_name}: OpenAPI spec exists but SDK doesn't cover this API" + ) + + return warnings + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> int: + parser = argparse.ArgumentParser(description="Check SDK drift against You.com OpenAPI specs") + parser.add_argument("--strict", action="store_true", help="Exit 1 on drift (for scheduled workflow)") + parser.add_argument("--verbose", action="store_true", help="Show all checks, even passing ones") + args = parser.parse_args() + + print("Fetching OpenAPI specs from you.com...") + try: + specs = fetch_specs() + except Exception as e: + print(f"ERROR: Could not fetch specs: {e}", file=sys.stderr) + return 1 if args.strict else 0 + + print(f"Found {len(specs)} specs: {', '.join(sorted(specs.keys()))}") + print() + + all_warnings: list[str] = [] + all_warnings += check_new_apis(specs) + all_warnings += check_endpoints(specs) + all_warnings += check_server_urls(specs) + all_warnings += check_enums(specs) + + if all_warnings: + print(f"DRIFT DETECTED ({len(all_warnings)} issue(s)):\n") + for w in all_warnings: + print(f" ⚠️ {w}") + print() + print("Review the above and update the SDK if needed.") + return 1 if args.strict else 0 + else: + print("No drift detected. SDK matches OpenAPI specs.") + if args.verbose: + print(f" Checked {len(specs)} specs, {len(EXPECTED_ENDPOINTS)} endpoints, " + f"{len(ENUM_CHECKS)} enum mappings, {len(EXPECTED_SERVERS)} server URLs.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 28d37fce444b919bdb81964bf1b11f12cdaf387d Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 17:55:56 -0700 Subject: [PATCH 12/20] Add POST /v1/answer route to Go mock server + mock server tests - New handler: tests/mockserver/internal/handler/pathpostv1answer.go - Register route in generated_handlers.go - Add TestAnswerMockServer class with 4 tests hitting the mock server (basic, freshness, boost_domains, async) - Existing MockTransport tests retained for error case coverage - Update tests/README.md test count (19 -> 23) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- tests/README.md | 2 +- .../internal/handler/generated_handlers.go | 1 + .../internal/handler/pathpostv1answer.go | 48 +++++++++++++ tests/test_answer.py | 71 +++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 tests/mockserver/internal/handler/pathpostv1answer.go diff --git a/tests/README.md b/tests/README.md index 0e5ba8d..f4e6146 100644 --- a/tests/README.md +++ b/tests/README.md @@ -80,7 +80,7 @@ Tests are organized into logical classes using pytest: - Optional format parameter - Error handling (unauthorized, forbidden, empty URLs) -**Answer API** (19 tests): +**Answer API** (23 tests): - Basic answer functionality - Answer with freshness, country, boost domains - Async answer diff --git a/tests/mockserver/internal/handler/generated_handlers.go b/tests/mockserver/internal/handler/generated_handlers.go index eea1109..db777e5 100644 --- a/tests/mockserver/internal/handler/generated_handlers.go +++ b/tests/mockserver/internal/handler/generated_handlers.go @@ -13,6 +13,7 @@ func GeneratedHandlers(ctx context.Context, dir *logging.HTTPFileDirectory, rt * return []*GeneratedHandler{ NewGeneratedHandler(ctx, http.MethodPost, "/v1/search", pathPostV1Search(dir, rt)), NewGeneratedHandler(ctx, http.MethodPost, "/v1/contents", pathPostV1Contents(dir, rt)), + NewGeneratedHandler(ctx, http.MethodPost, "/v1/answer", pathPostV1Answer(dir, rt)), NewGeneratedHandler(ctx, http.MethodPost, "/v1/research", pathPostV1Research(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/pathpostv1answer.go b/tests/mockserver/internal/handler/pathpostv1answer.go new file mode 100644 index 0000000..a417426 --- /dev/null +++ b/tests/mockserver/internal/handler/pathpostv1answer.go @@ -0,0 +1,48 @@ +package handler + +import ( + "log" + "mockserver/internal/handler/assert" + "mockserver/internal/logging" + "mockserver/internal/tracking" + "net/http" +) + +func pathPostV1Answer(dir *logging.HTTPFileDirectory, rt *tracking.RequestTracker) http.HandlerFunc { + _ = dir + _ = rt + return func(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 + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "answer": "The capital of France is Paris[[1]].", + "citations": [ + { + "source": "https://en.wikipedia.org/wiki/Paris", + "excerpts": ["Paris is the capital and most populous city of France."] + } + ], + "results": { + "web": [ + { + "url": "https://en.wikipedia.org/wiki/Paris", + "title": "Paris - Wikipedia", + "snippets": ["Paris is the capital and most populous city of France."], + "page_age": "2025-06-25T11:41:00Z" + } + ] + } + }`)) + } +} diff --git a/tests/test_answer.py b/tests/test_answer.py index fdcc863..e9d8338 100644 --- a/tests/test_answer.py +++ b/tests/test_answer.py @@ -1,10 +1,12 @@ """Tests for youdotcom.answer — POST /v1/answer.""" import json +import os import httpx import pytest +from tests.test_client import create_test_http_client from youdotcom import You from youdotcom.errors import ( ForbiddenResponseError, @@ -280,3 +282,72 @@ async def test_async_500_raises_internal_server_error(self): body = json.dumps({"detail": "internal server error"}) with pytest.raises(InternalServerErrorResponse): await _async_you(_make_handler(500, body)).answer_async(query="test") + + +# --------------------------------------------------------------------------- +# Mock server tests (POST /v1/answer via Go mock server on localhost:18080) +# --------------------------------------------------------------------------- + +@pytest.fixture +def server_url(): + return os.getenv("TEST_SERVER_URL", "http://localhost:18080") + + +@pytest.fixture +def api_key(): + return "test-api-key" + + +class TestAnswerMockServer: + """Tests for POST /v1/answer against the Go mock server.""" + + def test_basic_answer(self, server_url, api_key): + """Test basic answer query returns AnswerResponse with answer + citations.""" + client = create_test_http_client("post_/v1/answer") + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.answer(query="What is the capital of France?") + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + assert len(res.citations) > 0 + assert res.citations[0].source is not None + assert len(res.citations[0].source) > 0 + assert len(res.results.web) > 0 + assert res.results.web[0].url is not None + assert res.results.web[0].title is not None + + def test_answer_with_freshness(self, server_url, api_key): + """Test answer with freshness filter.""" + client = create_test_http_client("post_/v1/answer") + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.answer(query="Latest AI developments", freshness="week") + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + + def test_answer_with_boost_domains(self, server_url, api_key): + """Test answer with boost_domains.""" + client = create_test_http_client("post_/v1/answer") + with You(server_url=server_url, client=client, api_key_auth=api_key) as you: + res = you.answer( + query="Python type hints", + boost_domains=["python.org", "docs.python.org"], + ) + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + + @pytest.mark.asyncio + async def test_async_answer(self, server_url, api_key): + """Test async answer against mock server.""" + client = create_test_http_client("post_/v1/answer") + async with You( + server_url=server_url, + client=client, + async_client=httpx.AsyncClient(), + api_key_auth=api_key, + ) as you: + res = await you.answer_async(query="What is 2+2?") + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 From 4554bdb0c9a94aa024252b5d672101bb8f5fd32a Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 18:13:35 -0700 Subject: [PATCH 13/20] Address review feedback: drift check permissions + context manager - drift-check.yml: add explicit permissions (contents: read, issues: write) so GITHUB_TOKEN can create/close issues in scheduled runs - check_drift.py: use context manager for httpx.Client to guarantee cleanup on exception paths Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/drift-check.yml | 4 ++++ scripts/check_drift.py | 39 +++++++++++++++---------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml index 92be285..afa0ee4 100644 --- a/.github/workflows/drift-check.yml +++ b/.github/workflows/drift-check.yml @@ -6,6 +6,10 @@ on: - cron: "0 9 * * 1" workflow_dispatch: +permissions: + contents: read + issues: write + jobs: drift-check: runs-on: ubuntu-latest diff --git a/scripts/check_drift.py b/scripts/check_drift.py index 51cbcc1..c3de17e 100644 --- a/scripts/check_drift.py +++ b/scripts/check_drift.py @@ -75,26 +75,25 @@ def fetch_specs() -> dict[str, dict[str, Any]]: """Fetch all OpenAPI specs from the You.com docs index.""" - client = httpx.Client(timeout=30, follow_redirects=True) - r = client.get(OPENAPI_INDEX) - r.raise_for_status() - - # The index page is HTML with relative links like: - # Web Search - import re - matches = re.findall(r'href="openapi/([\w-]+)\.json"', r.text) - if not matches: - raise RuntimeError("Could not find any OpenAPI spec links in the index page") - - specs: dict[str, dict[str, Any]] = {} - for name in matches: - url = f"{OPENAPI_BASE}{name}.json" - resp = client.get(url) - resp.raise_for_status() - specs[name] = resp.json() - - client.close() - return specs + with httpx.Client(timeout=30, follow_redirects=True) as client: + r = client.get(OPENAPI_INDEX) + r.raise_for_status() + + # The index page is HTML with relative links like: + # Web Search + import re + matches = re.findall(r'href="openapi/([\w-]+)\.json"', r.text) + if not matches: + raise RuntimeError("Could not find any OpenAPI spec links in the index page") + + specs: dict[str, dict[str, Any]] = {} + for name in matches: + url = f"{OPENAPI_BASE}{name}.json" + resp = client.get(url) + resp.raise_for_status() + specs[name] = resp.json() + + return specs # --------------------------------------------------------------------------- From 8802f31804e2524eb9ef46c2de59ee125da02640 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 19:11:21 -0700 Subject: [PATCH 14/20] Add comprehensive CI checks + schema drift detection Drift check (check_drift.py): - Add request parameter drift: compares spec request body properties against SDK method signatures - Add response schema drift: compares spec 200 response schema fields against SDK pydantic model fields - Fix resolution for request bodies - Fix array response schema handling (contents returns List[]) Real drift found and fixed: - ResearchResponse missing 'warnings' field (List[str]) per OpenAPI spec - Added to model and TypedDict, backward-compatible (Optional, defaults None) CI improvements (test.yml): - Add Python 3.11 and 3.13 to test matrix (was 3.10 + 3.12 only) - Add pytest-cov coverage reporting (--cov=youdotcom --cov-report=term-missing) - Add pylint errors-only check (non-blocking, continue-on-error) - Add package build check job (python -m build --sdist --wheel) Dependabot (.github/dependabot.yml): - Weekly checks for pip and github-actions dependency updates Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/dependabot.yml | 12 ++ .github/workflows/test.yml | 34 +++- scripts/check_drift.py | 196 ++++++++++++++++++++++- src/youdotcom/models/researchresponse.py | 5 + 4 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e677fb4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f986137..d2b9d90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 @@ -29,7 +29,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -e . - pip install "mypy>=2.3.0" "pytest>=9.0.0" "pytest-asyncio>=1.0.0" + pip install "mypy>=2.3.0" "pytest>=9.0.0" "pytest-asyncio>=1.0.0" "pytest-cov" "pylint>=4.0.0" - name: Build and start mock server working-directory: tests/mockserver @@ -45,12 +45,38 @@ jobs: sleep 1 done - - name: Run tests - run: pytest tests/ -v --tb=short --ignore=tests/test_live.py --ignore=tests/test_performance.py + - name: Run tests with coverage + run: pytest tests/ -v --tb=short --ignore=tests/test_live.py --ignore=tests/test_performance.py --cov=youdotcom --cov-report=term-missing - name: Run mypy run: mypy src/youdotcom/ + - name: Run pylint (non-blocking) + run: pylint src/youdotcom/ --disable=all --enable=E --rcfile=/dev/null + continue-on-error: true + + build-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tool + run: pip install build + + - name: Build package + run: python -m build --sdist --wheel + + - name: Verify dist contents + run: | + ls -la dist/ + tar -tzf dist/*.tar.gz | head -20 + echo "Package builds successfully" + drift-check: runs-on: ubuntu-latest steps: diff --git a/scripts/check_drift.py b/scripts/check_drift.py index c3de17e..d97b8ab 100644 --- a/scripts/check_drift.py +++ b/scripts/check_drift.py @@ -7,6 +7,8 @@ 2. Server URLs — spec servers match SDK server constants 3. Enums — spec enum values match SDK enum classes 4. New APIs — specs the SDK doesn't cover yet (except known exceptions) + 5. Request parameters — spec request body properties match SDK method parameters + 6. Response schemas — spec response schema fields match SDK model fields Usage: python scripts/check_drift.py # Print warnings, exit 0 (CI, non-blocking) @@ -15,6 +17,8 @@ """ import argparse +import inspect +import re import sys from typing import Any @@ -68,6 +72,45 @@ ("research", "Country", "youdotcom.models", "Country"), ] +# SDK-internal parameters that aren't API params (excluded from drift comparison). +INTERNAL_PARAMS = {"retries", "server_url", "timeout_ms", "http_headers"} + +# Map (spec_name, method, path) -> SDK method info for schema checks. +# sdk_method: attribute name on You (or "SearchShim"/"ContentsShim" for shims) +# sdk_response_models: pydantic model class names in youdotcom.models +SCHEMA_CHECKS = [ + { + "spec": "web-search", + "endpoint": ("POST", "/v1/search"), + "sdk_method": "SearchShim.__call__", + "sdk_response_models": ["SearchResponse"], + }, + { + "spec": "contents", + "endpoint": ("POST", "/v1/contents"), + "sdk_method": "ContentsShim.__call__", + "sdk_response_models": ["ContentsResponse"], + }, + { + "spec": "answer", + "endpoint": ("POST", "/v1/answer"), + "sdk_method": "answer", + "sdk_response_models": ["AnswerResponse"], + }, + { + "spec": "research", + "endpoint": ("POST", "/v1/research"), + "sdk_method": "research", + "sdk_response_models": ["ResearchResponse", "TaskResponse"], + }, + { + "spec": "finance-research", + "endpoint": ("POST", "/v1/finance_research"), + "sdk_method": "finance_research", + "sdk_response_models": ["FinanceResearchResponse"], + }, +] + # --------------------------------------------------------------------------- # Spec fetching @@ -232,6 +275,154 @@ def check_new_apis(specs: dict[str, dict[str, Any]]) -> list[str]: return warnings +def _resolve_ref(ref: str, spec: dict[str, Any]) -> dict[str, Any]: + """Resolve a $ref pointer within an OpenAPI spec.""" + # Format: "#/components/schemas/SchemaName" + parts = ref.lstrip("#/").split("/") + obj: Any = spec + for part in parts: + obj = obj[part] + return obj + + +def _get_schema_properties(schema: dict[str, Any], spec: dict[str, Any]) -> set[str]: + """Extract top-level property names from a schema, resolving $ref, oneOf, and arrays.""" + if "$ref" in schema: + schema = _resolve_ref(schema["$ref"], spec) + + if "oneOf" in schema: + # Union type — collect properties from all branches + props: set[str] = set() + for branch in schema["oneOf"]: + props |= _get_schema_properties(branch, spec) + return props + + if schema.get("type") == "array" and "items" in schema: + # Array type — look at the items schema + return _get_schema_properties(schema["items"], spec) + + if "properties" in schema: + return set(schema["properties"].keys()) + + return set() + + +def _get_sdk_method_params(method_spec: str) -> set[str]: + """Get SDK method parameter names, excluding internal params.""" + import importlib + + if "." in method_spec and method_spec != "answer" and method_spec != "research" and method_spec != "finance_research": + # Shim classes: SearchShim.__call__, ContentsShim.__call__ + cls_name, method_name = method_spec.split(".") + mod = importlib.import_module("youdotcom._shims") + cls = getattr(mod, cls_name) + func = getattr(cls, method_name) + else: + mod = importlib.import_module("youdotcom") + cls = getattr(mod, "You") + func = getattr(cls, method_spec) + + sig = inspect.signature(func) + params = {p for p in sig.parameters if p != "self"} + return params - INTERNAL_PARAMS + + +def _get_sdk_model_fields(model_names: list[str]) -> set[str]: + """Get pydantic model field names for one or more model classes.""" + import importlib + mod = importlib.import_module("youdotcom.models") + fields: set[str] = set() + for name in model_names: + cls = getattr(mod, name) + fields |= set(cls.model_fields.keys()) + return fields + + +def check_request_params(specs: dict[str, dict[str, Any]]) -> list[str]: + """Check that spec request body properties match SDK method parameters.""" + warnings: list[str] = [] + + for check in SCHEMA_CHECKS: + spec_name = check["spec"] + method, path = check["endpoint"] + if spec_name not in specs: + continue + + spec = specs[spec_name] + path_item = spec.get("paths", {}).get(path, {}) + operation = path_item.get(method.lower(), {}) + request_body = operation.get("requestBody", {}) + content = request_body.get("content", {}).get("application/json", {}) + schema = content.get("schema", {}) + + if not schema: + continue + + # Resolve $ref if the request body is a reference + if "$ref" in schema: + schema = _resolve_ref(schema["$ref"], spec) + + spec_params = set(schema.get("properties", {}).keys()) + sdk_params = _get_sdk_method_params(check["sdk_method"]) + + missing_in_sdk = spec_params - sdk_params + missing_in_spec = sdk_params - spec_params + + if missing_in_sdk: + warnings.append( + f"[request] {spec_name} {method} {path}: spec has params {missing_in_sdk} " + f"which SDK doesn't accept" + ) + if missing_in_spec: + warnings.append( + f"[request] {spec_name} {method} {path}: SDK has params {missing_in_spec} " + f"which spec doesn't define" + ) + + return warnings + + +def check_response_schemas(specs: dict[str, dict[str, Any]]) -> list[str]: + """Check that spec 200 response schema fields match SDK model fields.""" + warnings: list[str] = [] + + for check in SCHEMA_CHECKS: + spec_name = check["spec"] + method, path = check["endpoint"] + if spec_name not in specs: + continue + + spec = specs[spec_name] + path_item = spec.get("paths", {}).get(path, {}) + operation = path_item.get(method.lower(), {}) + responses = operation.get("responses", {}) + ok_response = responses.get("200", {}) + content = ok_response.get("content", {}).get("application/json", {}) + schema = content.get("schema", {}) + + if not schema: + continue + + spec_fields = _get_schema_properties(schema, spec) + sdk_fields = _get_sdk_model_fields(check["sdk_response_models"]) + + missing_in_sdk = spec_fields - sdk_fields + missing_in_spec = sdk_fields - spec_fields + + if missing_in_sdk: + warnings.append( + f"[response] {spec_name} {method} {path}: spec has fields {missing_in_sdk} " + f"which SDK model doesn't have" + ) + if missing_in_spec: + warnings.append( + f"[response] {spec_name} {method} {path}: SDK model has fields {missing_in_spec} " + f"which spec doesn't define" + ) + + return warnings + + # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- @@ -257,6 +448,8 @@ def main() -> int: all_warnings += check_endpoints(specs) all_warnings += check_server_urls(specs) all_warnings += check_enums(specs) + all_warnings += check_request_params(specs) + all_warnings += check_response_schemas(specs) if all_warnings: print(f"DRIFT DETECTED ({len(all_warnings)} issue(s)):\n") @@ -269,7 +462,8 @@ def main() -> int: print("No drift detected. SDK matches OpenAPI specs.") if args.verbose: print(f" Checked {len(specs)} specs, {len(EXPECTED_ENDPOINTS)} endpoints, " - f"{len(ENUM_CHECKS)} enum mappings, {len(EXPECTED_SERVERS)} server URLs.") + f"{len(ENUM_CHECKS)} enum mappings, {len(EXPECTED_SERVERS)} server URLs, " + f"{len(SCHEMA_CHECKS)} schema/param mappings.") return 0 diff --git a/src/youdotcom/models/researchresponse.py b/src/youdotcom/models/researchresponse.py index f1a29b1..cf51042 100644 --- a/src/youdotcom/models/researchresponse.py +++ b/src/youdotcom/models/researchresponse.py @@ -86,8 +86,13 @@ class Output(BaseModel): class ResearchResponseTypedDict(TypedDict): output: OutputTypedDict r"""The research output containing the answer and sources.""" + warnings: NotRequired[List[str]] + r"""A list of warnings generated during research, such as source access issues or partial results. Empty when no warnings occurred.""" class ResearchResponse(BaseModel): output: Output r"""The research output containing the answer and sources.""" + + warnings: Optional[List[str]] = None + r"""A list of warnings generated during research, such as source access issues or partial results. Empty when no warnings occurred.""" From bd6f97caba2db3956a7af5e9d268f3d2f80afda6 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 20:17:13 -0700 Subject: [PATCH 15/20] Address 6 review findings: client cleanup, security serializer, docs, drift workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1: You.__aexit__ now closes SDK-owned sync client too; __exit__ closes SDK-owned async client (prevents socket leaks in async-only usage) P2: Fix Security.serialize_model() optional_fields — was 'ApiKeyAuth' but actual field key is 'api_key_auth' (no alias), causing null fields to be re-introduced despite exclude_none P2: Drift workflow now distinguishes 'DRIFT DETECTED' from 'ERROR:' before opening an issue (prevents false drift issues on network failures) P2: Search docs — removed stale GET recommendation and changed UsageSnippet method from get to post P2: Document test client cleanup expectations in create_test_http_client Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/drift-check.yml | 23 ++++++++++++++++------- docs/sdks/search/README.md | 4 +--- src/youdotcom/models/security.py | 2 +- src/youdotcom/sdk.py | 18 ++++++++++++++++++ tests/test_client.py | 6 ++++++ 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml index afa0ee4..5656273 100644 --- a/.github/workflows/drift-check.yml +++ b/.github/workflows/drift-check.yml @@ -36,20 +36,29 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Create drift label if it doesn't exist - gh label create drift --color FBCA04 --description "SDK drift from OpenAPI specs" 2>/dev/null || true - # Re-run to capture output for the issue body + # Re-run to capture output for classification OUTPUT=$(python scripts/check_drift.py --strict --verbose 2>&1 || true) - gh issue create \ - --title "SDK drift detected: OpenAPI specs vs SDK surface" \ - --body "The weekly drift check found differences between the You.com OpenAPI specs and the SDK. + + # Distinguish drift from fetch/runtime errors + if echo "$OUTPUT" | grep -q "DRIFT DETECTED"; then + # Create drift label if it doesn't exist + gh label create drift --color FBCA04 --description "SDK drift from OpenAPI specs" 2>/dev/null || true + gh issue create \ + --title "SDK drift detected: OpenAPI specs vs SDK surface" \ + --body "The weekly drift check found differences between the You.com OpenAPI specs and the SDK. \`\`\` ${OUTPUT} \`\`\` Review the warnings above and update the SDK as needed. Run \`python scripts/check_drift.py --verbose\` locally to reproduce." \ - --label "drift" + --label "drift" + elif echo "$OUTPUT" | grep -q "ERROR:"; then + echo "Drift check failed (not drift): $OUTPUT" + # Don't open an issue for transient errors + else + echo "Unexpected output: $OUTPUT" + fi - name: Close stale drift issues if: steps.drift.outcome == 'success' diff --git a/docs/sdks/search/README.md b/docs/sdks/search/README.md index 6e1548d..aed3b89 100644 --- a/docs/sdks/search/README.md +++ b/docs/sdks/search/README.md @@ -17,11 +17,9 @@ 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 - + ```python import os from youdotcom import You, models diff --git a/src/youdotcom/models/security.py b/src/youdotcom/models/security.py index 765679f..382082c 100644 --- a/src/youdotcom/models/security.py +++ b/src/youdotcom/models/security.py @@ -27,7 +27,7 @@ class Security(BaseModel): @model_serializer(mode="wrap") def serialize_model(self, handler): - optional_fields = set(["ApiKeyAuth"]) + optional_fields = set(["api_key_auth"]) serialized = handler(self) m = {} diff --git a/src/youdotcom/sdk.py b/src/youdotcom/sdk.py index fbf837e..fd33d69 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -5,6 +5,7 @@ from .sdkconfiguration import SDKConfiguration from .utils.logger import Logger, get_default_logger from .utils.retries import RetryConfig +import asyncio import httpx from typing import ( Any, @@ -159,6 +160,16 @@ def __exit__(self, exc_type, exc_val, exc_tb): ): self.sdk_configuration.client.close() self.sdk_configuration.client = None + # Also close SDK-owned async client + if ( + self.sdk_configuration.async_client is not None + and not self.sdk_configuration.async_client_supplied + ): + try: + asyncio.run(self.sdk_configuration.async_client.aclose()) + except RuntimeError: + pass + self.sdk_configuration.async_client = None async def __aexit__(self, exc_type, exc_val, exc_tb): if ( @@ -167,6 +178,13 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): ): await self.sdk_configuration.async_client.aclose() self.sdk_configuration.async_client = None + # Also close SDK-owned sync client + if ( + self.sdk_configuration.client is not None + and not self.sdk_configuration.client_supplied + ): + self.sdk_configuration.client.close() + self.sdk_configuration.client = None async def search_async(self, **kwargs: Any) -> models.SearchResponse: """Async variant of :meth:`search`.""" diff --git a/tests/test_client.py b/tests/test_client.py index 2bb4f61..998abe3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,6 +5,12 @@ def create_test_http_client(test_name: str) -> httpx.Client: + """Create a test HTTP client with tracking headers. + + The returned client is treated as SDK-supplied (not closed by You.__exit__). + Call ``client.close()`` after use, or use within a ``with You(...) as you:`` block + and close the client afterward. + """ return httpx.Client( headers={ "x-test-name": test_name, From 2570ab56354c2d5499b18cbf770972261138ca57 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 20:41:58 -0700 Subject: [PATCH 16/20] Address 2 P1 review findings: async client close + server_url docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1: Fix __exit__ async client close under running event loop — use get_running_loop() + run_coroutine_threadsafe() pattern (matching close_clients()) instead of asyncio.run() which raises RuntimeError in notebooks/async contexts P1: Clarify server_url constructor docstring — search/contents default to ydc-index.io and are not affected by the constructor server_url unless the per-method server_url argument is passed Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/youdotcom/sdk.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/youdotcom/sdk.py b/src/youdotcom/sdk.py index fd33d69..9bfe5a4 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -59,7 +59,7 @@ def __init__( :param api_key_auth: The api_key_auth required for authentication :param server_idx: The index of the server to use for all methods - :param server_url: The server URL to use for all methods + :param server_url: The server URL to use for all methods. Note: ``search()``/``search_async()`` and ``contents()``/``contents_async()`` default to ``ydc-index.io`` (via ``SEARCH_OP_SERVERS``/``CONTENTS_OP_SERVERS``) and are not affected by this parameter unless the per-method ``server_url`` argument is passed. :param url_params: Parameters to optionally template the server URL with :param client: The HTTP client to use for all synchronous methods :param async_client: The Async HTTP client to use for all asynchronous methods @@ -166,9 +166,13 @@ def __exit__(self, exc_type, exc_val, exc_tb): and not self.sdk_configuration.async_client_supplied ): try: - asyncio.run(self.sdk_configuration.async_client.aclose()) + loop = asyncio.get_running_loop() except RuntimeError: - pass + asyncio.run(self.sdk_configuration.async_client.aclose()) + else: + asyncio.run_coroutine_threadsafe( + self.sdk_configuration.async_client.aclose(), loop + ) self.sdk_configuration.async_client = None async def __aexit__(self, exc_type, exc_val, exc_tb): From 3c1bbb73df27b4c521f50682e7ee9bb6db6b94b0 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 20:57:13 -0700 Subject: [PATCH 17/20] Fix doc inaccuracies: host split, error count, mockserver refs - CHANGELOG: Remove 'Search/Contents host' from Changed (was already the case in 2.5.0, not a new change in 2.6.0) - CHANGELOG: Reword 'search() server_url fix' to accurately state that constructor server_url does NOT affect search/contents (only per-method server_url is respected) - README: Fix error count from 27 to 28 (3 httpx + 25 YouError subclasses) - MIGRATION: Add note that server URL behavior was already the case in 2.5.0, documented for reference - tests/mockserver/README: Replace stale 'auto-generated code from Speakeasy' with 'hand-maintained', remove deleted pathgetv1search.go from handler list, add all current handler files - PR description: Remove 'Search/Contents host split' from Breaking Changes (was already the case in 2.5.0) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- CHANGELOG.md | 3 +-- MIGRATION.md | 2 +- README.md | 2 +- tests/mockserver/README.md | 6 +++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b2510..f909280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,14 +28,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Search/Contents host**: `SEARCH_OP_SERVERS` and `CONTENTS_OP_SERVERS` point to `https://ydc-index.io`. The default server URL (used by answer, research, finance research) is `https://api.you.com`. `you.search()` and `you.contents()` now resolve to their own server constants instead of the default. `you.search()` targets `POST /v1/search` (keyed, requires an API key). - **422 error data model**: `UnprocessableEntityResponseErrorData` now includes optional `detail` (FastAPI validation array) and `errors` (JSON:API array) fields in addition to the existing `error` field. All three 422 response shapes deserialize without crashing. Backward compatible — existing code accessing `.error` still works. - **500 error data model**: `InternalServerErrorResponseData` now includes an optional `errors` field for JSON:API format 500 responses. Backward compatible. - **No longer generated by Speakeasy**: Removed all "Code generated by Speakeasy — DO NOT EDIT" disclaimers and the Speakeasy badge from the README. The SDK is now hand-maintained. - **Removed `YDCUserAgentOverrideHook`**: The hook existed to rewrite Speakeasy's default UA (`speakeasy-sdk/python ...`) to `youdotcom-python-sdk/{version}`. Now that `__user_agent__` is already `youdotcom-python-sdk/{version}`, `BaseSDK._build_request` sets it directly — the hook was a no-op. Integrations that need a custom UA still just set `client.sdk_configuration.user_agent`. - **`__user_agent__` derived from resolved `__version__`**: The user-agent string is now built from the package's resolved version at runtime rather than a hardcoded value. - **Dead code removal**: Deleted `_hooks/registration.py` (no-op `init_hooks`), `models/answerop.py` (unused `ANSWER_OP_SERVERS`), unused `importlib` and `TYPE_CHECKING` imports from `sdk.py`. Deleted all remaining agent model/error classes and their doc files. Deleted `overlays/python_overlay.yaml` (Speakeasy overlay, no longer used). -- **`search()` server_url fix**: `search()` and `search_async()` now use `self._get_url(models.SEARCH_OP_SERVERS[0], None)` (resolving to `ydc-index.io`) instead of a hardcoded server constant, so custom `server_url` passed to `You()` is respected (previously ignored for search only). +- **`search()` server_url handling**: `search()` and `search_async()` now use `self._get_url(models.SEARCH_OP_SERVERS[0], None)` (resolving to `ydc-index.io`) instead of a hardcoded server constant. The per-method `server_url` parameter is respected when passed; the constructor's `server_url` does not affect search/contents (they always default to `ydc-index.io`). This matches the behavior already present in 2.5.0. - **`answer()` type annotations**: `country` and `language` parameters changed from `Optional[str]` to `Optional[Union[str, models.Country]]` / `Optional[Union[str, models.Language]]` to match `search()` and reflect the actual enum constraint. String values are still accepted and normalized via `.upper()`. - **Dev dependencies updated**: mypy `1.15.0` → `>=2.3.0`, pylint `3.2.3` → `>=4.0.0`, pytest floor `>=8.0.0` → `>=9.0.0`, pytest-asyncio floor `>=0.24.0` → `>=1.0.0`. Runtime dependencies (httpx, httpcore, pydantic) unchanged — already at latest stable. diff --git a/MIGRATION.md b/MIGRATION.md index 5431698..9346911 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -39,7 +39,7 @@ you.search(query="...") ### Server URLs -`SEARCH_OP_SERVERS` and `CONTENTS_OP_SERVERS` point to `https://ydc-index.io` (used by `you.search()` and `you.contents()`). The default server URL (used by `you.answer()`, `you.research()`, `you.finance_research()`, etc.) is `https://api.you.com`. No code changes required — the SDK resolves the correct server per endpoint automatically. +`SEARCH_OP_SERVERS` and `CONTENTS_OP_SERVERS` point to `https://ydc-index.io` (used by `you.search()` and `you.contents()`). The default server URL (used by `you.answer()`, `you.research()`, `you.finance_research()`, etc.) is `https://api.you.com`. No code changes required — the SDK resolves the correct server per endpoint automatically. This behavior was already the case in 2.5.0; it is documented here for reference. ### Agents API Removed diff --git a/README.md b/README.md index 71f814a..fef8b3f 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,7 @@ with You( **Primary error:** * [`YouError`](./src/youdotcom/errors/youerror.py): The base class for HTTP error responses. -
Less common errors (27) +
Less common errors (28)
diff --git a/tests/mockserver/README.md b/tests/mockserver/README.md index bbea551..5610f36 100644 --- a/tests/mockserver/README.md +++ b/tests/mockserver/README.md @@ -1,6 +1,6 @@ # Mock Server -A generated HTTP mock server based on your OpenAPI Specification (OAS). This server contains a mixture of auto-generated code from Speakeasy and custom handlers for testing various scenarios including error responses. +A hand-maintained HTTP mock server based on the You.com OpenAPI Specification. This server contains custom handlers for testing various scenarios including error responses. ## Running Tests @@ -63,5 +63,5 @@ docker run -i -p 18080:18080 -t --rm mockserver -log-level=DEBUG ## Code Structure -- **Auto-generated**: Core mock server framework, routing, and SDK models (`internal/sdk/`, `internal/server/`, `internal/logging/`, `internal/tracking/`) -- **Custom**: Test-specific handlers with success and error scenarios for comprehensive testing (`internal/handler/pathgetv1search.go`, `internal/handler/pathpostv1contents.go`, `internal/handler/pathpostv1search.go`) +- **Core framework**: Mock server framework, routing, and SDK models (`internal/sdk/`, `internal/server/`, `internal/logging/`, `internal/tracking/`) +- **Custom handlers**: Test-specific handlers with success and error scenarios for comprehensive testing (`internal/handler/pathpostv1answer.go`, `internal/handler/pathpostv1contents.go`, `internal/handler/pathpostv1search.go`, `internal/handler/pathpostv1research.go`, `internal/handler/pathgetv1research.go`, `internal/handler/pathgetv1researchstream.go`, `internal/handler/pathpostv1financeresearch.go`) From a6f698f924325fa9fd58aff87c86eaa6b2cfdb02 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 20:59:21 -0700 Subject: [PATCH 18/20] Remove stale lite references from FinanceResearchEffort docstrings FinanceResearchEffort no longer includes a LITE tier (only DEEP and EXHAUSTIVE per official You.com docs). Removed stale 'lite' bullet from docstrings in finance_researchop.py (2 places) and sdk.py finance_research/finance_research_async (2 places). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/youdotcom/models/finance_researchop.py | 2 -- src/youdotcom/sdk.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/youdotcom/models/finance_researchop.py b/src/youdotcom/models/finance_researchop.py index f28fc6d..26c3325 100644 --- a/src/youdotcom/models/finance_researchop.py +++ b/src/youdotcom/models/finance_researchop.py @@ -19,7 +19,6 @@ class FinanceResearchRequestTypedDict(TypedDict): 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: - - `lite`: Returns answers quickly. Good for straightforward financial questions that just need a fast, reliable answer. - `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. """ @@ -36,7 +35,6 @@ class FinanceResearchRequest(BaseModel): 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: - - `lite`: Returns answers quickly. Good for straightforward financial questions that just need a fast, reliable answer. - `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. """ diff --git a/src/youdotcom/sdk.py b/src/youdotcom/sdk.py index 9bfe5a4..f5a6b97 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -1798,7 +1798,6 @@ def finance_research( :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: - - `lite`: Returns answers quickly. Good for straightforward financial questions that just need a fast, reliable answer. - `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 @@ -1923,7 +1922,6 @@ async def finance_research_async( :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: - - `lite`: Returns answers quickly. Good for straightforward financial questions that just need a fast, reliable answer. - `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 From 23903a8868b8869d5564960d1bd807e4c8c55998 Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 21:45:16 -0700 Subject: [PATCH 19/20] Address review findings: drift reliability, test leaks, dep bounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1: Fix drift-check.yml re-execution TOCTOU race — capture first run's output once via GITHUB_ENV instead of re-running the script a second time. Use distinct exit codes (0=no drift, 1=drift, 2=fetch error) instead of grepping free-form stdout, eliminating the silent-loss fallback where neither DRIFT DETECTED nor ERROR: appeared. P1: Fix test_answer.py resource leaks — test_async_answer was creating a real httpx.AsyncClient() (no MockTransport) that opened TCP sockets to the mock server and was never closed. Switched to MockTransport pattern (matching the rest of the file) with explicit try/finally cleanup. Also added client.close() to the 3 sync mock-server tests that were leaking create_test_http_client instances. P2: Fix check_drift.py request param schema extraction — was using bare schema.get('properties', {}).keys() while the response path used _get_schema_properties() which handles oneOf/anyOf/allOf and arrays. Unified to use _get_schema_properties for both paths. Also removed redundant inline 'import re' in fetch_specs() (already at module level). P2: Add upper bounds to pyproject.toml dev deps — mypy, pylint, pytest, pytest-asyncio were unbounded across semver-major bumps. Pinned upper bounds (<3, <5, <10, <2) so Dependabot can't float untested majors. P3: Add failure sentinel to test.yml mock-server readiness loop — if all 10 retries fail, the loop now exits 1 with a clear error message instead of silently falling through to pytest. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/drift-check.yml | 51 ++++++++++++++++++------------- .github/workflows/test.yml | 8 ++++- pyproject.toml | 10 +++--- scripts/check_drift.py | 16 +++++----- tests/test_answer.py | 30 +++++++++++------- 5 files changed, 67 insertions(+), 48 deletions(-) diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml index 5656273..078b296 100644 --- a/.github/workflows/drift-check.yml +++ b/.github/workflows/drift-check.yml @@ -28,40 +28,47 @@ jobs: - name: Run drift check id: drift - run: python scripts/check_drift.py --strict --verbose - continue-on-error: true + run: | + set +e + OUTPUT=$(python scripts/check_drift.py --strict --verbose 2>&1) + EXIT_CODE=$? + echo "$OUTPUT" + # Save output to GITHUB_ENV for downstream steps + { + echo "DRIFT_OUTPUT<> "$GITHUB_ENV" + echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" + exit 0 - name: Open issue on drift - if: steps.drift.outcome == 'failure' + if: steps.drift.outputs.exit_code == '1' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRIFT_OUTPUT: ${{ env.DRIFT_OUTPUT }} run: | - # Re-run to capture output for classification - OUTPUT=$(python scripts/check_drift.py --strict --verbose 2>&1 || true) - - # Distinguish drift from fetch/runtime errors - if echo "$OUTPUT" | grep -q "DRIFT DETECTED"; then - # Create drift label if it doesn't exist - gh label create drift --color FBCA04 --description "SDK drift from OpenAPI specs" 2>/dev/null || true - gh issue create \ - --title "SDK drift detected: OpenAPI specs vs SDK surface" \ - --body "The weekly drift check found differences between the You.com OpenAPI specs and the SDK. + # Create drift label if it doesn't exist + gh label create drift --color FBCA04 --description "SDK drift from OpenAPI specs" 2>/dev/null || true + gh issue create \ + --title "SDK drift detected: OpenAPI specs vs SDK surface" \ + --body "The weekly drift check found differences between the You.com OpenAPI specs and the SDK. \`\`\` - ${OUTPUT} + ${DRIFT_OUTPUT} \`\`\` Review the warnings above and update the SDK as needed. Run \`python scripts/check_drift.py --verbose\` locally to reproduce." \ - --label "drift" - elif echo "$OUTPUT" | grep -q "ERROR:"; then - echo "Drift check failed (not drift): $OUTPUT" - # Don't open an issue for transient errors - else - echo "Unexpected output: $OUTPUT" - fi + --label "drift" + + - name: Report fetch error + if: steps.drift.outputs.exit_code == '2' + run: | + echo "Drift check failed (fetch error, not drift):" + echo "${{ env.DRIFT_OUTPUT }}" - name: Close stale drift issues - if: steps.drift.outcome == 'success' + if: steps.drift.outputs.exit_code == '0' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2b9d90..ef17f2e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,21 +29,27 @@ jobs: run: | python -m pip install --upgrade pip pip install -e . - pip install "mypy>=2.3.0" "pytest>=9.0.0" "pytest-asyncio>=1.0.0" "pytest-cov" "pylint>=4.0.0" + pip install "mypy>=2.3.0,<3" "pytest>=9.0.0,<10" "pytest-asyncio>=1.0.0,<2" "pytest-cov" "pylint>=4.0.0,<5" - name: Build and start mock server working-directory: tests/mockserver run: | go build -o mockserver . ./mockserver & + SERVER_READY=false for i in $(seq 1 10); do if curl -sf http://localhost:18080/_mockserver/health; then echo "mock server ready" + SERVER_READY=true break fi echo "waiting for mock server..." sleep 1 done + if [ "$SERVER_READY" = false ]; then + echo "::error::mock server failed to start within 10 seconds" + exit 1 + fi - name: Run tests with coverage run: pytest tests/ -v --tb=short --ignore=tests/test_live.py --ignore=tests/test_performance.py --cov=youdotcom --cov-report=term-missing diff --git a/pyproject.toml b/pyproject.toml index d3c2916..9acdec9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,11 +14,11 @@ license = { text = "Apache-2.0" } [dependency-groups] dev = [ - "mypy >=2.3.0", - "pylint >=4.0.0", - "pyright >=1.1.398", - "pytest >=9.0.0", - "pytest-asyncio >=1.0.0", + "mypy >=2.3.0, <3", + "pylint >=4.0.0, <5", + "pyright >=1.1.398, <2", + "pytest >=9.0.0, <10", + "pytest-asyncio >=1.0.0, <2", ] [tool.setuptools.packages.find] diff --git a/scripts/check_drift.py b/scripts/check_drift.py index d97b8ab..197edf8 100644 --- a/scripts/check_drift.py +++ b/scripts/check_drift.py @@ -124,7 +124,6 @@ def fetch_specs() -> dict[str, dict[str, Any]]: # The index page is HTML with relative links like: # Web Search - import re matches = re.findall(r'href="openapi/([\w-]+)\.json"', r.text) if not matches: raise RuntimeError("Could not find any OpenAPI spec links in the index page") @@ -358,11 +357,7 @@ def check_request_params(specs: dict[str, dict[str, Any]]) -> list[str]: if not schema: continue - # Resolve $ref if the request body is a reference - if "$ref" in schema: - schema = _resolve_ref(schema["$ref"], spec) - - spec_params = set(schema.get("properties", {}).keys()) + spec_params = _get_schema_properties(schema, spec) sdk_params = _get_sdk_method_params(check["sdk_method"]) missing_in_sdk = spec_params - sdk_params @@ -429,7 +424,7 @@ def check_response_schemas(specs: dict[str, dict[str, Any]]) -> list[str]: def main() -> int: parser = argparse.ArgumentParser(description="Check SDK drift against You.com OpenAPI specs") - parser.add_argument("--strict", action="store_true", help="Exit 1 on drift (for scheduled workflow)") + parser.add_argument("--strict", action="store_true", help="Exit 1 on drift, 2 on fetch error (for scheduled workflow)") parser.add_argument("--verbose", action="store_true", help="Show all checks, even passing ones") args = parser.parse_args() @@ -438,7 +433,8 @@ def main() -> int: specs = fetch_specs() except Exception as e: print(f"ERROR: Could not fetch specs: {e}", file=sys.stderr) - return 1 if args.strict else 0 + print("RESULT: fetch_error") + return 2 print(f"Found {len(specs)} specs: {', '.join(sorted(specs.keys()))}") print() @@ -457,9 +453,11 @@ def main() -> int: print(f" ⚠️ {w}") print() print("Review the above and update the SDK if needed.") - return 1 if args.strict else 0 + print("RESULT: drift") + return 1 else: print("No drift detected. SDK matches OpenAPI specs.") + print("RESULT: no_drift") if args.verbose: print(f" Checked {len(specs)} specs, {len(EXPECTED_ENDPOINTS)} endpoints, " f"{len(ENUM_CHECKS)} enum mappings, {len(EXPECTED_SERVERS)} server URLs, " diff --git a/tests/test_answer.py b/tests/test_answer.py index e9d8338..c57ad76 100644 --- a/tests/test_answer.py +++ b/tests/test_answer.py @@ -315,6 +315,7 @@ def test_basic_answer(self, server_url, api_key): assert len(res.results.web) > 0 assert res.results.web[0].url is not None assert res.results.web[0].title is not None + client.close() def test_answer_with_freshness(self, server_url, api_key): """Test answer with freshness filter.""" @@ -324,6 +325,7 @@ def test_answer_with_freshness(self, server_url, api_key): assert isinstance(res, AnswerResponse) assert len(res.answer) > 0 + client.close() def test_answer_with_boost_domains(self, server_url, api_key): """Test answer with boost_domains.""" @@ -336,18 +338,24 @@ def test_answer_with_boost_domains(self, server_url, api_key): assert isinstance(res, AnswerResponse) assert len(res.answer) > 0 + client.close() @pytest.mark.asyncio async def test_async_answer(self, server_url, api_key): """Test async answer against mock server.""" - client = create_test_http_client("post_/v1/answer") - async with You( - server_url=server_url, - client=client, - async_client=httpx.AsyncClient(), - api_key_auth=api_key, - ) as you: - res = await you.answer_async(query="What is 2+2?") - - assert isinstance(res, AnswerResponse) - assert len(res.answer) > 0 + async_client = httpx.AsyncClient(transport=httpx.MockTransport(_make_handler(200))) + client = httpx.Client(transport=httpx.MockTransport(_make_handler(200))) + try: + async with You( + server_url=server_url, + client=client, + async_client=async_client, + api_key_auth=api_key, + ) as you: + res = await you.answer_async(query="What is 2+2?") + + assert isinstance(res, AnswerResponse) + assert len(res.answer) > 0 + finally: + await async_client.aclose() + client.close() From a6ed49c1c74deb8430bea9e90d57aa6c400169ee Mon Sep 17 00:00:00 2001 From: Tyler Eastman Date: Wed, 5 Aug 2026 22:34:05 -0700 Subject: [PATCH 20/20] Release as 3.0.0: fail-fast auth, lifecycle + normalization fixes Addresses the review findings on the v2.6.0 release PR. Versioning: - Bump 2.6.0 -> 3.0.0. The Agents API, `search_post()`, `search_helpers`, and the sub-SDK classes are removed, which is breaking and needs a major version under the SemVer policy the CHANGELOG claims to follow. Behavior: - An empty `api_key_auth` now raises ValueError at construction instead of silently falling back to the environment. Every endpoint requires a key, so `""` is never valid -- it means a key was expected and none arrived, nearly always `os.getenv("YDC_API_KEY", "")` with the variable unset. Falling back there ran the request under a different identity than the code asked for. Callables are checked on first use. - `search(language=None)` sends no language again. Omitting the argument still uses the API default (EN); previously an explicit None also resolved to EN, with no way to opt out of the filter. - `__exit__`/`__aexit__` no longer let a failing client close replace the exception propagating out of the block, matching `close_clients`. - `country`/`language`/`safesearch`/`livecrawl`/`livecrawl_formats`/ `freshness` are now genuinely case-normalized, which the docs already claimed. Also fixes the `to` separator in uppercase date ranges. - `search_async()`/`contents_async()` are real methods with full signatures instead of `**kwargs: Any` wrappers that erased them for type checkers and IDEs. Clears the one pylint error in the tree. Tests (154 -> 249): - New coverage for debug-log redaction, empty-key rejection, dual client close and its non-masking property, and the normalization matrix including the deprecated `unified()` path. Redaction and the key handling were both shipped untested. Docs: - Replace all 52 `os.getenv("YDC_API_KEY", "")` examples, which the new key handling turns into an error. - CHANGELOG gains Removed/Security/Fixed sections; drops an entry for a file that never existed on main and two no-op entries. Security is now redaction alone -- the key change is fail-fast, not a closed hole. - MIGRATION gains an action-required table plus sections on key resolution, client lifecycle, redaction, and `language`. CI: - Drift issues update an existing open issue instead of filing a duplicate weekly; body written via --body-file so the code fence renders; a broken checker exits 3 and fails the job rather than reporting "no drift" forever; --strict and --verbose mean something again. - Dev tooling installs from the pyproject dependency group instead of a duplicated list. pylint gates now that the tree is clean. dependabot switched pip -> uv so uv.lock actually gets updated. Verified: 249 tests pass, mypy clean (80 files), pylint 10.00/10, drift check green against the live specs. Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 7 +- .github/workflows/drift-check.yml | 76 +++++++--- .github/workflows/test.yml | 13 +- .gitignore | 4 + CHANGELOG.md | 44 ++++-- MIGRATION.md | 106 +++++++++++++- README.md | 40 ++++-- USAGE.md | 4 +- docs/sdks/answer/README.md | 2 +- docs/sdks/contentssdk/README.md | 12 +- docs/sdks/search/README.md | 2 +- docs/sdks/you/README.md | 62 ++++----- pyproject.toml | 5 +- scripts/check_drift.py | 87 +++++++++--- src/youdotcom/_shims.py | 13 +- src/youdotcom/_version.py | 2 +- src/youdotcom/sdk.py | 224 ++++++++++++++++++++---------- tests/test_client_lifecycle.py | 133 ++++++++++++++++++ tests/test_live.py | 2 +- tests/test_param_normalization.py | 191 +++++++++++++++++++++++++ tests/test_redaction.py | 113 +++++++++++++++ tests/test_security_env.py | 95 ++++++++++++- uv.lock | 131 ++++++++++++++++- 23 files changed, 1169 insertions(+), 199 deletions(-) create mode 100644 tests/test_client_lifecycle.py create mode 100644 tests/test_param_normalization.py create mode 100644 tests/test_redaction.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e677fb4..fba7158 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,15 @@ version: 2 updates: - - package-ecosystem: pip + # The project resolves with uv and commits uv.lock; the `pip` ecosystem reads + # pyproject.toml but leaves the lockfile stale, so use `uv` instead. + - package-ecosystem: uv directory: / schedule: interval: weekly open-pull-requests-limit: 5 + groups: + dev-dependencies: + dependency-type: development - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml index 078b296..2194fef 100644 --- a/.github/workflows/drift-check.yml +++ b/.github/workflows/drift-check.yml @@ -10,6 +10,10 @@ permissions: contents: read issues: write +concurrency: + group: drift-check + cancel-in-progress: false + jobs: drift-check: runs-on: ubuntu-latest @@ -26,6 +30,9 @@ jobs: python -m pip install --upgrade pip pip install -e . + # Exit codes (see scripts/check_drift.py): 0 no drift, 1 drift, + # 2 specs unreachable, 3 the check itself is broken. Keeping these + # distinct is what stops a traceback from being filed as "drift". - name: Run drift check id: drift run: | @@ -33,46 +40,79 @@ jobs: OUTPUT=$(python scripts/check_drift.py --strict --verbose 2>&1) EXIT_CODE=$? echo "$OUTPUT" - # Save output to GITHUB_ENV for downstream steps + # Random delimiter: a fixed "EOF" would break if it appeared in the output. + DELIM="drift_$(openssl rand -hex 8)" { - echo "DRIFT_OUTPUT<> "$GITHUB_ENV" echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" exit 0 - - name: Open issue on drift + - name: Report or update drift issue if: steps.drift.outputs.exit_code == '1' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRIFT_OUTPUT: ${{ env.DRIFT_OUTPUT }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - # Create drift label if it doesn't exist - gh label create drift --color FBCA04 --description "SDK drift from OpenAPI specs" 2>/dev/null || true - gh issue create \ - --title "SDK drift detected: OpenAPI specs vs SDK surface" \ - --body "The weekly drift check found differences between the You.com OpenAPI specs and the SDK. + set -euo pipefail + gh label create drift --color FBCA04 \ + --description "SDK drift from OpenAPI specs" 2>/dev/null || true - \`\`\` - ${DRIFT_OUTPUT} - \`\`\` + # Build the body in a file so the fenced block isn't indented by the + # surrounding YAML block scalar (indented fences don't render). + { + echo "The drift check found differences between the You.com OpenAPI specs and the SDK." + echo + echo '```' + echo "${DRIFT_OUTPUT}" + echo '```' + echo + echo "Run \`python scripts/check_drift.py --verbose\` locally to reproduce." + echo + echo "[Workflow run](${RUN_URL})" + } > drift-body.md - Review the warnings above and update the SDK as needed. Run \`python scripts/check_drift.py --verbose\` locally to reproduce." \ - --label "drift" + # Update the existing issue instead of filing a duplicate every week. + EXISTING=$(gh issue list --label drift --state open \ + --json number --jq '.[0].number // empty') + if [ -n "${EXISTING}" ]; then + echo "Updating existing drift issue #${EXISTING}" + gh issue comment "${EXISTING}" --body-file drift-body.md + else + gh issue create \ + --title "SDK drift detected: OpenAPI specs vs SDK surface" \ + --body-file drift-body.md \ + --label drift + fi - name: Report fetch error if: steps.drift.outputs.exit_code == '2' run: | - echo "Drift check failed (fetch error, not drift):" - echo "${{ env.DRIFT_OUTPUT }}" + echo "::warning::Drift check could not fetch the OpenAPI specs (transient, not drift)." + echo "${DRIFT_OUTPUT}" + env: + DRIFT_OUTPUT: ${{ env.DRIFT_OUTPUT }} + + # A broken checker is worse than drift: it reports "no drift" forever. + # Fail the job so the run goes red and someone looks at it. + - name: Fail on broken drift check + if: steps.drift.outputs.exit_code != '0' && steps.drift.outputs.exit_code != '1' && steps.drift.outputs.exit_code != '2' + run: | + echo "::error::Drift check exited ${{ steps.drift.outputs.exit_code }} — the check itself failed." + echo "${DRIFT_OUTPUT}" + exit 1 + env: + DRIFT_OUTPUT: ${{ env.DRIFT_OUTPUT }} - name: Close stale drift issues if: steps.drift.outputs.exit_code == '0' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Close any open drift issues since no drift was detected - gh issue list --label "drift" --state open --json number --jq '.[].number' | while read -r num; do + # No drift this run, so anything still open has been resolved. + gh issue list --label drift --state open --json number --jq '.[].number' | while read -r num; do gh issue close "$num" --comment "No drift detected in the latest check. Closing." done diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef17f2e..ceca03f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,10 @@ on: push: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest @@ -27,9 +31,11 @@ jobs: - name: Install dependencies run: | + # --group needs pip >= 25.1; the upgrade above guarantees it. Reading + # the group from pyproject keeps CI and local dev on one dependency list. python -m pip install --upgrade pip pip install -e . - pip install "mypy>=2.3.0,<3" "pytest>=9.0.0,<10" "pytest-asyncio>=1.0.0,<2" "pytest-cov" "pylint>=4.0.0,<5" + pip install --group dev - name: Build and start mock server working-directory: tests/mockserver @@ -57,9 +63,10 @@ jobs: - name: Run mypy run: mypy src/youdotcom/ - - name: Run pylint (non-blocking) + # Errors only, and the tree is clean at 10.00/10 — so this gates rather + # than reporting into the void the way a continue-on-error step would. + - name: Run pylint run: pylint src/youdotcom/ --disable=all --enable=E --rcfile=/dev/null - continue-on-error: true build-check: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 1a8e80a..8729423 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ venv/ src/*.egg-info/ **/__pycache__/ .pytest_cache/ +.coverage +.coverage.* +htmlcov/ +.mypy_cache/ .python-version .DS_Store pyrightconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f909280..a35b3e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,20 @@ 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.6.0] - 2026-08-04 +## [3.0.0] - 2026-08-06 + +This release removes the Agents API and the sub-SDK classes, which is a +breaking change and therefore a major version bump. Pin `youdotcom<3` if you +still depend on the Agents API. + +### Removed + +- **Agents API**: The `you.agents()` / `you.agents_async()` direct methods and the `you.agents.runs` sub-SDK shim have been removed. All Agents API model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, `AgentRunsResponseOutput`, `AgentRunsStreamingResponse`, `AgentRunsResponseWebSearchResult`, `ComputeTool`, `WebSearchTool`, `ResearchTool`, `ReportVerbosity`, `SearchEffort`, `Verbosity`, and streaming event models) and error classes (`AgentRuns400ResponseError`, `AgentRuns401ResponseError`, `AgentRuns422ResponseError`) have been deleted from `youdotcom.models` and `youdotcom.errors`. +- **`search_helpers` module**: The standalone `search_helpers.search()` function has been merged into `you.search()`. Import `from youdotcom import You` and call `you.search(query=...)` directly. +- **`you.search_post()`**: Use `you.search()`. +- **`__gen_version__` / `SPEAKEASY_GENERATOR_VERSION`**: These exports are gone from `youdotcom` and `youdotcom._version`. `__version__`, `__title__`, `__openapi_doc_version__`, and `__user_agent__` are unaffected. +- **`YDCUserAgentOverrideHook` and `_hooks/registration.py`**: The hook existed to rewrite Speakeasy's default UA (`speakeasy-sdk/python ...`) to `youdotcom-python-sdk/{version}`. Now that `__user_agent__` is already `youdotcom-python-sdk/{version}`, `BaseSDK._build_request` sets it directly — the hook was a no-op. Integrations that need a custom UA still just set `client.sdk_configuration.user_agent`. +- **Dead code**: Unused `importlib` and `TYPE_CHECKING` imports from `sdk.py`, all remaining agent model/error classes and their doc files, and `overlays/python_overlay.yaml` (Speakeasy overlay, no longer used). ### Deprecations @@ -18,25 +31,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | `you.contents.generate(urls=...)` | `you.contents(urls=...)` | | `you.contents.generate_async(urls=...)` | `you.contents_async(urls=...)` | -- **`search_helpers` module removed**: The standalone `search_helpers.search()` function has been merged into `you.search()`. Import `from youdotcom import You` and call `you.search(query=...)` directly. -- **Agents API removed**: The `you.agents()` / `you.agents_async()` direct methods and the `you.agents.runs` sub-SDK shim have been removed. All Agents API model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, `AgentRunsResponseOutput`, `AgentRunsStreamingResponse`, `AgentRunsResponseWebSearchResult`, `ComputeTool`, `WebSearchTool`, `ResearchTool`, `ReportVerbosity`, `SearchEffort`, `Verbosity`, and streaming event models) and error classes (`AgentRuns400ResponseError`, `AgentRuns401ResponseError`, `AgentRuns422ResponseError`) have been deleted from `youdotcom.models` and `youdotcom.errors`. - ### Added -- **Answer API**: New direct method `you.answer()` / `you.answer_async()` for `POST /v1/answer`. Returns a synthesized markdown answer with inline citations (`[[1, 2]]`), a citations array (source URLs + supporting excerpts), and web results. Accepts `query` (required), `freshness`, `country`, `language`, `include_domains`, `exclude_domains`, `boost_domains`. Requires an API key. Country and language accept plain strings (e.g. `"us"`, `"en"`) and are normalized to uppercase automatically. +- **Answer API**: New direct method `you.answer()` / `you.answer_async()` for `POST /v1/answer`. Returns a synthesized markdown answer with inline citations (`[[1, 2]]`), a citations array (source URLs + supporting excerpts), and web results. Accepts `query` (required), `freshness`, `country`, `language`, `include_domains`, `exclude_domains`, `boost_domains`. Requires an API key. - **`PaymentRequiredResponseError`**: New first-class error class for HTTP 402 responses, with data model `PaymentRequiredResponseErrorData` (`error`, `message`, `upgrade_url`, `limit`, `used`, `period`, `reset_at`). Used by the answer 402 handler. +- **Case-insensitive enum parameters**: `country`, `language`, `safesearch`, `livecrawl`, `livecrawl_formats`, and `freshness` accept plain strings in any case and are normalized to the casing the API expects (`"us"` → `"US"`, `"STRICT"` → `"strict"`). Callers no longer need to import enum classes. Enum members still work. +- **SDK drift check** (`scripts/check_drift.py`): Compares the You.com OpenAPI specs against the SDK surface (endpoints, server URLs, enum values, request params, response fields). Runs non-blocking on every PR and weekly on a schedule, opening an issue when drift is found. + +### Security + +- **Debug-log redaction**: `Authorization`, `X-API-Key`, `Cookie`, and `Set-Cookie` are replaced with `[REDACTED]` before request/response headers are written to the debug logger. Previously a debug logger would capture the API key in plaintext. Applies to both the sync and async paths. ### Changed +- **An empty API key raises instead of falling back to the environment**: `You(api_key_auth="")` — or a blank string, or a callable returning one — now raises `ValueError`. Every endpoint requires a key, so an empty string is never a valid argument; it means a key was expected and none arrived. Previously the SDK fell through to `YDC_API_KEY` / `YOU_API_KEY_AUTH`, which could run the request under a different identity than the code appeared to request. Passing `None` (or omitting the argument) remains the supported way to read the key from the environment. + + In practice this surfaces as `os.getenv("YDC_API_KEY", "")` with the variable unset; use `os.getenv("YDC_API_KEY")`. All documentation examples have been updated accordingly. +- **Both context managers now close both transports**: `__exit__` disposes of the SDK-owned async client in addition to the sync one, and `__aexit__` does the reverse — previously whichever transport the block didn't use was leaked. As a consequence, an instance is no longer usable after leaving either block, including for calls of the other flavor: `with You(...) as you: ...` followed by `await you.search_async(...)` will fail. Create a separate client, or use `async with`, if you need both. Caller-supplied transports are still never closed by the SDK. +- **`search(language=None)` sends no language**: Omitting the argument uses the API default (`"EN"`) as before; passing `None` explicitly now omits the field entirely rather than falling back to the default. - **422 error data model**: `UnprocessableEntityResponseErrorData` now includes optional `detail` (FastAPI validation array) and `errors` (JSON:API array) fields in addition to the existing `error` field. All three 422 response shapes deserialize without crashing. Backward compatible — existing code accessing `.error` still works. - **500 error data model**: `InternalServerErrorResponseData` now includes an optional `errors` field for JSON:API format 500 responses. Backward compatible. - **No longer generated by Speakeasy**: Removed all "Code generated by Speakeasy — DO NOT EDIT" disclaimers and the Speakeasy badge from the README. The SDK is now hand-maintained. -- **Removed `YDCUserAgentOverrideHook`**: The hook existed to rewrite Speakeasy's default UA (`speakeasy-sdk/python ...`) to `youdotcom-python-sdk/{version}`. Now that `__user_agent__` is already `youdotcom-python-sdk/{version}`, `BaseSDK._build_request` sets it directly — the hook was a no-op. Integrations that need a custom UA still just set `client.sdk_configuration.user_agent`. - **`__user_agent__` derived from resolved `__version__`**: The user-agent string is now built from the package's resolved version at runtime rather than a hardcoded value. -- **Dead code removal**: Deleted `_hooks/registration.py` (no-op `init_hooks`), `models/answerop.py` (unused `ANSWER_OP_SERVERS`), unused `importlib` and `TYPE_CHECKING` imports from `sdk.py`. Deleted all remaining agent model/error classes and their doc files. Deleted `overlays/python_overlay.yaml` (Speakeasy overlay, no longer used). -- **`search()` server_url handling**: `search()` and `search_async()` now use `self._get_url(models.SEARCH_OP_SERVERS[0], None)` (resolving to `ydc-index.io`) instead of a hardcoded server constant. The per-method `server_url` parameter is respected when passed; the constructor's `server_url` does not affect search/contents (they always default to `ydc-index.io`). This matches the behavior already present in 2.5.0. -- **`answer()` type annotations**: `country` and `language` parameters changed from `Optional[str]` to `Optional[Union[str, models.Country]]` / `Optional[Union[str, models.Language]]` to match `search()` and reflect the actual enum constraint. String values are still accepted and normalized via `.upper()`. -- **Dev dependencies updated**: mypy `1.15.0` → `>=2.3.0`, pylint `3.2.3` → `>=4.0.0`, pytest floor `>=8.0.0` → `>=9.0.0`, pytest-asyncio floor `>=0.24.0` → `>=1.0.0`. Runtime dependencies (httpx, httpcore, pydantic) unchanged — already at latest stable. +- **Dev dependencies updated**: mypy `1.15.0` → `>=2.3.0,<3`, pylint `3.2.3` → `>=4.0.0,<5`, pytest floor `>=8.0.0` → `>=9.0.0,<10`, pytest-asyncio floor `>=0.24.0` → `>=1.0.0,<2`. Runtime dependencies (httpx, httpcore, pydantic) unchanged — already at latest stable. + +### Fixed + +- **`SDKConfiguration.retry_config` default**: The field used `pydantic.Field(default_factory=...)` on a stdlib `@dataclass`, which does not interpret a `FieldInfo` and left the raw object as the default. Now uses `dataclasses.field`. +- **`Security` serializer dropped the wrong key**: `serialize_model` listed `"ApiKeyAuth"` in `optional_fields`, but the field is named `api_key_auth`, so the name never matched and a `None` key was serialized instead of omitted. +- **`_populate_from_globals` name comparison**: Used `is not` to compare strings, which depends on interning and could silently fail to match. Now uses `!=`. +- **Async methods are fully typed**: `search_async()` and `contents_async()` were thin `**kwargs: Any` wrappers, which erased their signatures for type checkers and IDEs. They are now the real implementations with explicit parameters. ## [2.5.0] - 2026-07-20 diff --git a/MIGRATION.md b/MIGRATION.md index 9346911..d42ad15 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,9 +1,100 @@ # Migration Guide -## 2.5.0 → 2.6.0 +## 2.5.0 → 3.0.0 > **This release adds the Answer API, removes the Agents API, and makes `search()` a direct method on `You`.** The old sub-SDK patterns still work but emit `DeprecationWarning`. Migrate at your convenience. +### Action required + +Most of this release is additive, but four changes can alter the behavior of +code that upgrades without edits: + +| Change | Who is affected | What to do | +|--------|-----------------|------------| +| Agents API removed | Anyone calling `you.agents...` | Pin `youdotcom<3`, or call the REST endpoint directly | +| An empty API key now raises | Anyone using `os.getenv("YDC_API_KEY", "")` | Drop the `""` default — see [API key resolution](#api-key-resolution) | +| Context managers close both transports | Anyone mixing sync and async calls on one instance | Use one instance per flavor — see [Client lifecycle](#client-lifecycle) | +| `search(language=None)` no longer defaults to `EN` | Anyone passing `language=None` explicitly | Omit the argument to keep the `EN` default | + +### API key resolution + +`You(api_key_auth="")` now raises `ValueError` instead of quietly reading the +environment. Every You.com endpoint requires a key, so an empty string is never +a valid argument — it means a key was expected and none arrived: + +```python +# Before (2.5.x): fell through to the YDC_API_KEY / YOU_API_KEY_AUTH lookup, +# so the request ran under whatever the environment held. +# After (3.0.0): ValueError, naming the likely cause. +You(api_key_auth=os.getenv("YDC_API_KEY", "")) + +# Correct in 3.0.0 — None is how you ask for the environment lookup: +You(api_key_auth=os.getenv("YDC_API_KEY")) +``` + +The old fallback was worth removing because it could run a request under a +*different* identity than the code appeared to request — most visibly when +`YDC_API_KEY` is unset but the legacy `YOU_API_KEY_AUTH` is still set, or on +shared CI runners. This is a fail-fast change, not a new unauthenticated mode: +there is no way to call these endpoints without a key. + +| `api_key_auth` value | Behavior | +| -------------------- | -------- | +| omitted, or `None` | Reads `YDC_API_KEY`, then the legacy `YOU_API_KEY_AUTH` | +| a non-empty string, or a callable returning one | That key is used; no environment lookup | +| `""` (or blank), or a callable returning one | Raises `ValueError` | + +A callable is resolved lazily, so a callable that returns an empty key raises +on first use rather than at construction. + +### Client lifecycle + +`You` creates both a sync and an async transport. Previously each context +manager closed only its own, leaking the other. Both now close both: + +```python +with You(api_key_auth=key) as you: + you.search(query="...") +# Both transports are now closed and dropped. +``` + +If you were relying on a single instance for both flavors, note that an +instance is unusable after either block exits — including for calls of the +other flavor: + +```python +# Broken in 3.0.0: +with You(api_key_auth=key) as you: + you.search(query="...") +await you.search_async(query="...") # transports already closed + +# Use `async with` for async work, or create a separate instance. +async with You(api_key_auth=key) as you: + await you.search_async(query="...") +``` + +Transports you supply yourself (`You(client=...)`, `You(async_client=...)`) +are still never closed by the SDK — you remain responsible for them. + +### Debug logging redacts credentials + +If you attach a `debug_logger`, `Authorization`, `X-API-Key`, `Cookie`, and +`Set-Cookie` are now logged as `[REDACTED]`. Previously the API key appeared +in plaintext in those logs. No code change is needed; if you were parsing +debug output for header values, those four are no longer recoverable. + +### `search(language=...)` + +Omitting `language` still sends the API default (`EN`). Passing `None` +explicitly now means "send no language at all" instead of falling back to that +default: + +```python +you.search(query="...") # language=EN (unchanged) +you.search(query="...", language="fr") # language=FR (unchanged) +you.search(query="...", language=None) # 2.5.x: EN → 3.0.0: field omitted +``` + ### Answer API New direct method `you.answer()` for `POST /v1/answer`: @@ -33,7 +124,7 @@ The standalone `search_helpers` module has been removed. Its `search()` / `searc # Before (2.5.x): from youdotcom.search_helpers import search # search(you, query="...") -# After (2.6.0): +# After (3.0.0): you.search(query="...") ``` @@ -74,7 +165,14 @@ The SDK is now hand-maintained. All "Code generated by Speakeasy — DO NOT EDIT ### Sub-SDK Deprecation -The sub-SDK layer was an abstraction that added unnecessary indirection — `you.search.unified()` routed through extra layers before reaching the HTTP client. In 2.6.0 these chains are collapsed into direct methods on `You`. The new methods also have simpler signatures: `country`, `language`, `safesearch`, `livecrawl`, and `freshness` accept plain strings (e.g. `"us"`, `"en"`, `"strict"`) and are normalized automatically, so callers no longer need to import enum classes. +The sub-SDK layer was an abstraction that added unnecessary indirection — `you.search.unified()` routed through extra layers before reaching the HTTP client. In 3.0.0 these chains are collapsed into direct methods on `You`. The new methods also have simpler signatures: `country`, `language`, `safesearch`, `livecrawl`, `livecrawl_formats`, and `freshness` accept plain strings in any case and are normalized to the casing the API expects, so callers no longer need to import enum classes: + +| Parameter | Normalized to | Example | +|-----------|---------------|---------| +| `country`, `language` | uppercase | `"us"` → `"US"`, `"zh-hans"` → `"ZH-HANS"` | +| `safesearch`, `livecrawl`, `livecrawl_formats`, `freshness` | lowercase | `"STRICT"` → `"strict"`, `"2026-01-01TO2026-02-01"` → `"2026-01-01to2026-02-01"` | + +Enum members (`Country.US`, `SafeSearch.STRICT`, …) continue to work unchanged. The old patterns still work but emit `DeprecationWarning` and delegate to the new methods. Migrate at your convenience: @@ -436,7 +534,7 @@ res = you.contents.generate(urls=["https://example.com"], crawl_timeout=5) ## 1.x to 2.0 -> **Note:** If you are upgrading to 2.6.0+, the Agents API (`you.agents.runs.create()`) and all agent model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, `ResponseCreated`, `ResponseStarting`, `ResponseOutputTextDelta`, `ResponseOutputContentFull`, `ResponseDone`, `ReportVerbosity`, `SearchEffort`, `AgentRuns401ResponseError`, etc.) have been removed entirely. The migration steps below that reference agent imports and calls are no longer valid — see the [2.5.0 to 2.6.0](#250--260) section above. The Search and Contents API changes below still apply. +> **Note:** If you are upgrading to 3.0.0+, the Agents API (`you.agents.runs.create()`) and all agent model classes (`ExpressAgentRunsRequest`, `AdvancedAgentRunsRequest`, `CustomAgentRunsRequest`, `AgentRunsBatchResponse`, `ResponseCreated`, `ResponseStarting`, `ResponseOutputTextDelta`, `ResponseOutputContentFull`, `ResponseDone`, `ReportVerbosity`, `SearchEffort`, `AgentRuns401ResponseError`, etc.) have been removed entirely. The migration steps below that reference agent imports and calls are no longer valid — see the [2.5.0 to 3.0.0](#250--300) section above. The Search and Contents API changes below still apply. This guide helps you upgrade your code from You.com Python SDK 1.x to 2.0. diff --git a/README.md b/README.md index fef8b3f..92bb1e2 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -161,7 +161,7 @@ from youdotcom import You, models async def main(): async with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = await you.search_async(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -198,7 +198,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -213,6 +213,24 @@ with You( print(res) ``` + +### How the key is resolved + +| `api_key_auth` value | Behavior | +| -------------------- | -------- | +| omitted, or `None` | Reads the `YDC_API_KEY` environment variable, then the legacy `YOU_API_KEY_AUTH` | +| a non-empty string, or a callable returning one | That key is used; no environment lookup | +| `""` (or blank), or a callable returning one | Raises `ValueError` | + +Every endpoint requires an API key, so an empty string is never a valid +argument — it means a key was expected and none arrived. The SDK raises rather +than reading the environment, because falling back there would run the request +under whatever identity the environment happens to hold instead of the one the +code asked for. + +In practice this shows up as `os.getenv("YDC_API_KEY", "")` with the variable +unset. Use `os.getenv("YDC_API_KEY")` — passing `None` is how you ask for the +environment lookup. @@ -253,7 +271,7 @@ from youdotcom.utils import eventstreaming with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: # Stream updates for a background research task via SSE @@ -286,7 +304,7 @@ from youdotcom.utils import BackoffStrategy, RetryConfig with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -312,7 +330,7 @@ from youdotcom.utils import BackoffStrategy, RetryConfig with You( retry_config=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False), - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -350,7 +368,7 @@ from youdotcom import You, errors, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = None try: @@ -442,7 +460,7 @@ from youdotcom import You, models with You( server_url="https://api.you.com", - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -461,7 +479,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -573,7 +591,7 @@ from youdotcom import You def main(): with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: # Rest of application here... @@ -582,7 +600,7 @@ def main(): async def amain(): async with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: # Rest of application here... ``` diff --git a/USAGE.md b/USAGE.md index 17f7411..b6f6c53 100644 --- a/USAGE.md +++ b/USAGE.md @@ -6,7 +6,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -34,7 +34,7 @@ from youdotcom import You, models async def main(): async with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = await you.search_async(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ diff --git a/docs/sdks/answer/README.md b/docs/sdks/answer/README.md index a0edaf1..6b7d022 100644 --- a/docs/sdks/answer/README.md +++ b/docs/sdks/answer/README.md @@ -22,7 +22,7 @@ from youdotcom import You with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.answer(query="What are the main causes of the 2008 financial crisis?") diff --git a/docs/sdks/contentssdk/README.md b/docs/sdks/contentssdk/README.md index 66f6c28..6004310 100644 --- a/docs/sdks/contentssdk/README.md +++ b/docs/sdks/contentssdk/README.md @@ -26,7 +26,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.contents.generate(urls=[ @@ -49,7 +49,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.contents.generate(urls=[ @@ -72,7 +72,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.contents.generate(urls=[ @@ -95,7 +95,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.contents.generate(urls=[ @@ -118,7 +118,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.contents.generate(urls=[ @@ -141,7 +141,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.contents.generate(urls=[ diff --git a/docs/sdks/search/README.md b/docs/sdks/search/README.md index aed3b89..c23f7ee 100644 --- a/docs/sdks/search/README.md +++ b/docs/sdks/search/README.md @@ -26,7 +26,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search.unified(query="Your query", count=10, language=models.Language.EN, exclude_domains="spam-site.com,other-site.com", boost_domains="nytimes.com,wired.com", crawl_timeout=10) diff --git a/docs/sdks/you/README.md b/docs/sdks/you/README.md index 690a6e8..650bd6d 100644 --- a/docs/sdks/you/README.md +++ b/docs/sdks/you/README.md @@ -35,7 +35,7 @@ import os from youdotcom import You with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.answer(query="What is the capital of France?") print(res) @@ -54,7 +54,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -78,7 +78,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -102,7 +102,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -126,7 +126,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -150,7 +150,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -174,7 +174,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -198,7 +198,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.search(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ @@ -259,7 +259,7 @@ import os from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.contents(urls=["https://example.com"], formats=[models.ContentsFormats.MARKDOWN]) print(res) @@ -278,7 +278,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -296,7 +296,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -314,7 +314,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -332,7 +332,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -350,7 +350,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -368,7 +368,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -386,7 +386,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -404,7 +404,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -422,7 +422,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -440,7 +440,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.research(input="", research_effort=models.ResearchEffort.STANDARD) @@ -487,7 +487,7 @@ from youdotcom import You with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + api_key_auth=os.getenv("YDC_API_KEY"), ) as you: res = you.get_research_task(task_id="586a9bc3-2c52-499c-a61d-be3cc9170c51") @@ -531,7 +531,7 @@ from youdotcom import You with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -579,7 +579,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -597,7 +597,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -615,7 +615,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -633,7 +633,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -651,7 +651,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -669,7 +669,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -687,7 +687,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -705,7 +705,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -723,7 +723,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) @@ -741,7 +741,7 @@ from youdotcom import You, models with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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) diff --git a/pyproject.toml b/pyproject.toml index 9acdec9..542e8e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "youdotcom" -version = "2.6.0" +version = "3.0.0" description = "The official You.com Python SDK." authors = [{ name = "You.com" },] readme = "README.md" @@ -13,12 +13,15 @@ dependencies = [ license = { text = "Apache-2.0" } [dependency-groups] +# The single source of truth for dev tooling — CI installs this group with +# `pip install --group dev` rather than repeating the list. dev = [ "mypy >=2.3.0, <3", "pylint >=4.0.0, <5", "pyright >=1.1.398, <2", "pytest >=9.0.0, <10", "pytest-asyncio >=1.0.0, <2", + "pytest-cov >=6.0.0, <8", ] [tool.setuptools.packages.find] diff --git a/scripts/check_drift.py b/scripts/check_drift.py index 197edf8..d15baa2 100644 --- a/scripts/check_drift.py +++ b/scripts/check_drift.py @@ -14,12 +14,19 @@ python scripts/check_drift.py # Print warnings, exit 0 (CI, non-blocking) python scripts/check_drift.py --strict # Exit 1 on drift (scheduled workflow) python scripts/check_drift.py --verbose # Show all checks, even passing ones + +Exit codes: + 0 no drift (or drift without --strict) + 1 drift detected (--strict only) + 2 specs could not be fetched — transient, not drift + 3 the check itself failed to run — a bug, not drift """ import argparse import inspect import re import sys +import traceback from typing import Any import httpx @@ -277,7 +284,7 @@ def check_new_apis(specs: dict[str, dict[str, Any]]) -> list[str]: def _resolve_ref(ref: str, spec: dict[str, Any]) -> dict[str, Any]: """Resolve a $ref pointer within an OpenAPI spec.""" # Format: "#/components/schemas/SchemaName" - parts = ref.lstrip("#/").split("/") + parts = ref.removeprefix("#/").split("/") obj: Any = spec for part in parts: obj = obj[part] @@ -307,11 +314,14 @@ def _get_schema_properties(schema: dict[str, Any], spec: dict[str, Any]) -> set[ def _get_sdk_method_params(method_spec: str) -> set[str]: - """Get SDK method parameter names, excluding internal params.""" + """Get SDK method parameter names, excluding internal params. + + A dotted ``method_spec`` names a shim class method (``SearchShim.__call__``); + a bare one names a method on ``You`` (``answer``). + """ import importlib - if "." in method_spec and method_spec != "answer" and method_spec != "research" and method_spec != "finance_research": - # Shim classes: SearchShim.__call__, ContentsShim.__call__ + if "." in method_spec: cls_name, method_name = method_spec.split(".") mod = importlib.import_module("youdotcom._shims") cls = getattr(mod, cls_name) @@ -422,9 +432,39 @@ def check_response_schemas(specs: dict[str, dict[str, Any]]) -> list[str]: # Main # --------------------------------------------------------------------------- +# Every check, in run order. Named so --verbose can report each one individually. +CHECKS = [ + ("coverage", check_new_apis), + ("endpoints", check_endpoints), + ("servers", check_server_urls), + ("enums", check_enums), + ("request params", check_request_params), + ("response schemas", check_response_schemas), +] + + +def run_checks(specs: dict[str, dict[str, Any]], verbose: bool) -> list[str]: + """Run every check, optionally reporting the outcome of each one.""" + all_warnings: list[str] = [] + for name, check in CHECKS: + warnings = check(specs) + if verbose: + status = f"{len(warnings)} warning(s)" if warnings else "ok" + print(f" [{name}] {status}") + all_warnings += warnings + if verbose: + print() + return all_warnings + + def main() -> int: parser = argparse.ArgumentParser(description="Check SDK drift against You.com OpenAPI specs") - parser.add_argument("--strict", action="store_true", help="Exit 1 on drift, 2 on fetch error (for scheduled workflow)") + parser.add_argument( + "--strict", + action="store_true", + help="Exit 1 when drift is found (for the scheduled workflow). " + "Without it, drift is reported but the exit status stays 0.", + ) parser.add_argument("--verbose", action="store_true", help="Show all checks, even passing ones") args = parser.parse_args() @@ -432,6 +472,7 @@ def main() -> int: try: specs = fetch_specs() except Exception as e: + # Transient: the docs site was unreachable or served something unexpected. print(f"ERROR: Could not fetch specs: {e}", file=sys.stderr) print("RESULT: fetch_error") return 2 @@ -439,13 +480,17 @@ def main() -> int: print(f"Found {len(specs)} specs: {', '.join(sorted(specs.keys()))}") print() - all_warnings: list[str] = [] - all_warnings += check_new_apis(specs) - all_warnings += check_endpoints(specs) - all_warnings += check_server_urls(specs) - all_warnings += check_enums(specs) - all_warnings += check_request_params(specs) - all_warnings += check_response_schemas(specs) + try: + all_warnings = run_checks(specs, args.verbose) + except Exception as e: + # A bug in the checker itself, or an SDK import failure. This must not + # look like drift: the caller distinguishes it by exit code so the + # scheduled workflow can flag a broken check instead of silently + # reporting "no drift" (or filing an issue full of traceback). + print(f"ERROR: Drift check failed to run: {e!r}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + print("RESULT: internal_error") + return 3 if all_warnings: print(f"DRIFT DETECTED ({len(all_warnings)} issue(s)):\n") @@ -454,15 +499,15 @@ def main() -> int: print() print("Review the above and update the SDK if needed.") print("RESULT: drift") - return 1 - else: - print("No drift detected. SDK matches OpenAPI specs.") - print("RESULT: no_drift") - if args.verbose: - print(f" Checked {len(specs)} specs, {len(EXPECTED_ENDPOINTS)} endpoints, " - f"{len(ENUM_CHECKS)} enum mappings, {len(EXPECTED_SERVERS)} server URLs, " - f"{len(SCHEMA_CHECKS)} schema/param mappings.") - return 0 + return 1 if args.strict else 0 + + print("No drift detected. SDK matches OpenAPI specs.") + print("RESULT: no_drift") + if args.verbose: + print(f" Checked {len(specs)} specs, {len(EXPECTED_ENDPOINTS)} endpoints, " + f"{len(ENUM_CHECKS)} enum mappings, {len(EXPECTED_SERVERS)} server URLs, " + f"{len(SCHEMA_CHECKS)} schema/param mappings.") + return 0 if __name__ == "__main__": diff --git a/src/youdotcom/_shims.py b/src/youdotcom/_shims.py index ea39b6f..9e8656c 100644 --- a/src/youdotcom/_shims.py +++ b/src/youdotcom/_shims.py @@ -40,6 +40,9 @@ class SearchShim: ``you.search(query=...)`` → delegates to ``you._search_impl()`` (no warning). ``you.search.unified(query=...)`` → delegates with DeprecationWarning. + + The async counterpart is a plain method on ``You`` (``you.search_async()``); + only the deprecated ``unified_async()`` spelling lives here. """ def __init__(self, you: Any) -> None: @@ -53,7 +56,7 @@ def __call__( freshness: Optional[str] = None, offset: Optional[int] = None, country: Optional[str] = None, - language: Optional[str] = None, + language: OptionalNullable[str] = UNSET, safesearch: Optional[str] = None, livecrawl: Optional[str] = None, livecrawl_formats: Optional[Iterable[str]] = None, @@ -94,7 +97,7 @@ def unified( freshness: Optional[str] = None, offset: Optional[int] = None, country: Optional[str] = None, - language: Optional[str] = None, + language: OptionalNullable[str] = UNSET, safesearch: Optional[str] = None, livecrawl: Optional[str] = None, livecrawl_formats: Optional[Iterable[str]] = None, @@ -137,7 +140,7 @@ async def unified_async( freshness: Optional[str] = None, offset: Optional[int] = None, country: Optional[str] = None, - language: Optional[str] = None, + language: OptionalNullable[str] = UNSET, safesearch: Optional[str] = None, livecrawl: Optional[str] = None, livecrawl_formats: Optional[Iterable[str]] = None, @@ -152,7 +155,7 @@ async def unified_async( ) -> models.SearchResponse: _warn("you.search.unified_async()", "you.search_async()") - return await self._you._search_async_impl( + return await self._you.search_async( query=query, count=count, freshness=freshness, @@ -243,7 +246,7 @@ async def generate_async( http_headers: Optional[Mapping[str, str]] = None, ) -> List[models.ContentsResponse]: _warn("you.contents.generate_async()", "you.contents_async()") - return await self._you._contents_async_impl( + return await self._you.contents_async( urls=urls, formats=formats, crawl_timeout=crawl_timeout, diff --git a/src/youdotcom/_version.py b/src/youdotcom/_version.py index 7e641ce..bbf7cf6 100644 --- a/src/youdotcom/_version.py +++ b/src/youdotcom/_version.py @@ -2,7 +2,7 @@ import importlib.metadata __title__: str = "youdotcom" -__version__: str = "2.6.0" +__version__: str = "3.0.0" __openapi_doc_version__: str = "1.0.0" try: diff --git a/src/youdotcom/sdk.py b/src/youdotcom/sdk.py index f5a6b97..d0df774 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -27,6 +27,44 @@ from youdotcom.utils.unmarshal_json_response import unmarshal_json_response +def _upper(value: Any) -> Any: + """Normalize a plain-string enum value to its uppercase spelling. + + Used for ``country`` and ``language``, whose enum members are uppercase + (``"us"`` -> ``"US"``). Non-strings (and ``None``) pass through untouched; + enum members are ``str`` subclasses, so they normalize to themselves. + """ + return value.upper() if isinstance(value, str) else value + + +def _lower(value: Any) -> Any: + """Normalize a plain-string enum value to its lowercase spelling. + + Used for ``safesearch``, ``livecrawl``, and ``freshness``, whose enum + members are lowercase (``"STRICT"`` -> ``"strict"``). Date-range freshness + values are unaffected apart from the ``to`` separator, which the API + expects in lowercase anyway. + """ + return value.lower() if isinstance(value, str) else value + + +def _lower_each(values: Optional[Iterable[Any]]) -> Optional[List[Any]]: + """Apply :func:`_lower` to every item of an optional iterable.""" + if values is None: + return None + return [_lower(v) for v in values] + + +_EMPTY_API_KEY_MESSAGE = ( + "api_key_auth was an empty string. Every You.com endpoint requires an API " + 'key, so this is never valid. If you meant to read the key from the ' + "environment, pass None or omit the argument -- the SDK then reads " + "YDC_API_KEY (falling back to YOU_API_KEY_AUTH). This usually comes from " + 'os.getenv("YDC_API_KEY", "") with the variable unset; use ' + 'os.getenv("YDC_API_KEY") instead.' +) + + class You(BaseSDK): r"""You.com API: Unified API for search, answers, research, and content from You.com Get the best search results from web and news sources @@ -88,17 +126,31 @@ def __init__( ), "The provided async_client must implement the AsyncHttpClient protocol." security: Any = None - # An explicit empty string (e.g. You(api_key_auth=os.getenv("YDC_API_KEY", ""))) - # is treated as an opt-out: we construct Security with api_key_auth=None so no - # X-API-Key header is sent AND we don't fall back to env vars. Only api_key_auth=None - # (the default) allows env fallback via get_security_from_env. + # Every endpoint requires a key, so an empty string is never a valid + # argument -- it means the caller thought they were passing a key and + # weren't, almost always `os.getenv("YDC_API_KEY", "")` with the variable + # unset. Falling back to the environment there would run the request + # under whatever identity the environment happens to hold, which is not + # the one the code asked for. Reject it at construction instead, where + # the message can name the actual mistake. `None` (the default) is the + # supported way to ask for the environment lookup. + if isinstance(api_key_auth, str) and not api_key_auth.strip(): + raise ValueError(_EMPTY_API_KEY_MESSAGE) + if callable(api_key_auth): - # pylint: disable=unnecessary-lambda-assignment - security = lambda: models.Security( - api_key_auth=(api_key_auth() or None) - ) + + def _resolve_security() -> models.Security: + key = api_key_auth() + if not key or not key.strip(): + raise ValueError( + "The api_key_auth callable returned an empty API key. " + + _EMPTY_API_KEY_MESSAGE + ) + return models.Security(api_key_auth=key) + + security = _resolve_security elif api_key_auth is not None: - security = models.Security(api_key_auth=(api_key_auth or None)) + security = models.Security(api_key_auth=api_key_auth) else: security = None @@ -153,50 +205,62 @@ def __enter__(self): async def __aenter__(self): return self - def __exit__(self, exc_type, exc_val, exc_tb): - if ( - self.sdk_configuration.client is not None - and not self.sdk_configuration.client_supplied - ): - self.sdk_configuration.client.close() + def _close_sync_client(self) -> None: + """Close the SDK-owned sync client, if any, and drop the reference. + + Errors are swallowed (as in ``close_clients``) so that a failure while + tearing down the client cannot replace an exception propagating out of + the ``with`` block. + """ + client = self.sdk_configuration.client self.sdk_configuration.client = None - # Also close SDK-owned async client - if ( - self.sdk_configuration.async_client is not None - and not self.sdk_configuration.async_client_supplied - ): + if client is not None and not self.sdk_configuration.client_supplied: try: - loop = asyncio.get_running_loop() - except RuntimeError: - asyncio.run(self.sdk_configuration.async_client.aclose()) - else: - asyncio.run_coroutine_threadsafe( - self.sdk_configuration.async_client.aclose(), loop - ) + client.close() + except Exception: # pylint: disable=broad-exception-caught + pass + + def _close_async_client_from_sync(self) -> None: + """Close the SDK-owned async client from a synchronous context. + + Mirrors ``close_clients``: if a loop is already running, the close is + scheduled on it; otherwise a throwaway loop drives it to completion. + Errors are swallowed for the same reason as ``_close_sync_client``. + """ + client = self.sdk_configuration.async_client self.sdk_configuration.async_client = None + if client is None or self.sdk_configuration.async_client_supplied: + return + try: + loop = asyncio.get_running_loop() + except RuntimeError: + try: + asyncio.run(client.aclose()) + except Exception: # pylint: disable=broad-exception-caught + pass + else: + try: + asyncio.run_coroutine_threadsafe(client.aclose(), loop) + except Exception: # pylint: disable=broad-exception-caught + pass - async def __aexit__(self, exc_type, exc_val, exc_tb): - if ( - self.sdk_configuration.async_client is not None - and not self.sdk_configuration.async_client_supplied - ): - await self.sdk_configuration.async_client.aclose() + async def _close_async_client(self) -> None: + """Close the SDK-owned async client, awaiting completion.""" + client = self.sdk_configuration.async_client self.sdk_configuration.async_client = None - # Also close SDK-owned sync client - if ( - self.sdk_configuration.client is not None - and not self.sdk_configuration.client_supplied - ): - self.sdk_configuration.client.close() - self.sdk_configuration.client = None + if client is not None and not self.sdk_configuration.async_client_supplied: + try: + await client.aclose() + except Exception: # pylint: disable=broad-exception-caught + pass - async def search_async(self, **kwargs: Any) -> models.SearchResponse: - """Async variant of :meth:`search`.""" - return await self._search_async_impl(**kwargs) + def __exit__(self, exc_type, exc_val, exc_tb): + self._close_sync_client() + self._close_async_client_from_sync() - async def contents_async(self, **kwargs: Any) -> List[models.ContentsResponse]: - """Async variant of :meth:`contents`.""" - return await self._contents_async_impl(**kwargs) + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self._close_async_client() + self._close_sync_client() def answer( self, @@ -255,9 +319,9 @@ def answer( body: dict = dict( query=query, - freshness=freshness, - country=country.upper() if isinstance(country, str) else country, - language=language.upper() if isinstance(language, str) else language, + freshness=_lower(freshness), + country=_upper(country), + language=_upper(language), 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]]), @@ -403,9 +467,9 @@ async def answer_async( body: dict = dict( query=query, - freshness=freshness, - country=country.upper() if isinstance(country, str) else country, - language=language.upper() if isinstance(language, str) else language, + freshness=_lower(freshness), + country=_upper(country), + language=_upper(language), 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]]), @@ -608,7 +672,7 @@ def _contents_impl( raise errors.YouDefaultError("Unexpected response received", http_res) - async def _contents_async_impl( + async def contents_async( self, *, urls: Optional[Iterable[str]] = None, @@ -730,7 +794,7 @@ def _search_impl( freshness: Optional[str] = None, offset: Optional[int] = None, country: Optional[str] = None, - language: Optional[str] = None, + language: OptionalNullable[str] = UNSET, safesearch: Optional[str] = None, livecrawl: Optional[str] = None, livecrawl_formats: Optional[Iterable[str]] = None, @@ -745,9 +809,10 @@ def _search_impl( ) -> models.SearchResponse: r"""Search via POST /v1/search. - Enum-typed parameters (``country``, ``safesearch``, ``livecrawl``, - ``freshness``) accept plain strings -- pydantic coerces them when - building the request body, so callers don't need to import enum classes. + Enum-typed parameters (``country``, ``language``, ``safesearch``, + ``livecrawl``, ``livecrawl_formats``, ``freshness``) accept plain + strings in any case -- the SDK normalizes them to the casing the API + expects, so callers don't need to import enum classes. :param query: The search query used to retrieve relevant results from the web. :param count: Max results per section (1-100). @@ -755,7 +820,8 @@ def _search_impl( ``"YYYY-MM-DDtoYYYY-MM-DD"``. :param offset: Pagination offset (multiples of ``count``). :param country: Country code for geographical focus. - :param language: BCP 47 language code (default ``"en"``). + :param language: BCP 47 language code. Omit the argument to use the API + default (``"en"``); pass ``None`` to send no language at all. :param safesearch: ``"strict"``, ``"moderate"``, or ``"off"``. :param livecrawl: ``"web"``, ``"news"``, or ``"all"``. :param livecrawl_formats: ``["html"]``, ``["markdown"]``, or both. @@ -781,21 +847,24 @@ def _search_impl( body: dict[str, Any] = dict( query=query, count=count, - freshness=freshness, + freshness=_lower(freshness), offset=offset, - country=country.upper() if isinstance(country, str) else country, - safesearch=safesearch, - livecrawl=livecrawl, + country=_upper(country), + safesearch=_lower(safesearch), + livecrawl=_lower(livecrawl), livecrawl_formats=utils.unmarshal( - livecrawl_formats, Optional[List[models.LiveCrawlFormats]] + _lower_each(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, ) - if language is not None: - body["language"] = language.upper() if isinstance(language, str) else language + # UNSET (the default) leaves the field off entirely so SearchRequestBody's + # own `Language.EN` default applies. An explicit None is passed through and + # dropped during serialization, which sends no language at all. + if language is not UNSET: + body["language"] = _upper(language) request = models.SearchRequestBody(**body) req = self._build_request( @@ -875,7 +944,7 @@ def _search_impl( raise errors.YouDefaultError("Unexpected response received", http_res) - async def _search_async_impl( + async def search_async( self, *, query: str, @@ -883,7 +952,7 @@ async def _search_async_impl( freshness: Optional[str] = None, offset: Optional[int] = None, country: Optional[str] = None, - language: Optional[str] = None, + language: OptionalNullable[str] = UNSET, safesearch: Optional[str] = None, livecrawl: Optional[str] = None, livecrawl_formats: Optional[Iterable[str]] = None, @@ -898,7 +967,9 @@ async def _search_async_impl( ) -> models.SearchResponse: r"""Search via POST /v1/search. - Async variant of :meth:`search`. + Async variant of ``you.search()``. See :meth:`_search_impl` for the + full parameter reference; ``language`` defaults to ``UNSET`` (use the + API default ``"en"``) and accepts ``None`` to send no language at all. """ base_url = None url_variables = None @@ -913,21 +984,24 @@ async def _search_async_impl( body: dict[str, Any] = dict( query=query, count=count, - freshness=freshness, + freshness=_lower(freshness), offset=offset, - country=country.upper() if isinstance(country, str) else country, - safesearch=safesearch, - livecrawl=livecrawl, + country=_upper(country), + safesearch=_lower(safesearch), + livecrawl=_lower(livecrawl), livecrawl_formats=utils.unmarshal( - livecrawl_formats, Optional[List[models.LiveCrawlFormats]] + _lower_each(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, ) - if language is not None: - body["language"] = language.upper() if isinstance(language, str) else language + # UNSET (the default) leaves the field off entirely so SearchRequestBody's + # own `Language.EN` default applies. An explicit None is passed through and + # dropped during serialization, which sends no language at all. + if language is not UNSET: + body["language"] = _upper(language) request = models.SearchRequestBody(**body) req = self._build_request_async( diff --git a/tests/test_client_lifecycle.py b/tests/test_client_lifecycle.py new file mode 100644 index 0000000..527b16c --- /dev/null +++ b/tests/test_client_lifecycle.py @@ -0,0 +1,133 @@ +"""Tests for client teardown in `You.__exit__` / `You.__aexit__`. + +Both exits close *both* transports, so a sync `with` block also disposes of +the SDK-owned async client (and vice versa). Two properties matter: + + 1. SDK-owned clients get closed; caller-supplied ones do not. + 2. A failure while closing never replaces the exception propagating out of + the `with` block — that would hide the error the user actually cares + about behind an unrelated teardown error. +""" + +import httpx +import pytest + +from youdotcom import You + + +class _RecordingClient(httpx.Client): + closed = False + + def close(self): + type(self).closed = True + super().close() + + +class _RecordingAsyncClient(httpx.AsyncClient): + closed = False + + async def aclose(self): + type(self).closed = True + await super().aclose() + + +class _BoomClient(httpx.Client): + def close(self): + raise RuntimeError("sync close boom") + + +class _BoomAsyncClient(httpx.AsyncClient): + async def aclose(self): + raise RuntimeError("async close boom") + + +def _owned(you: You) -> You: + """Mark both transports as SDK-owned so the exit paths will close them.""" + you.sdk_configuration.client_supplied = False + you.sdk_configuration.async_client_supplied = False + return you + + +class TestSyncExit: + def test_closes_both_clients(self): + sync_client = _RecordingClient() + async_client = _RecordingAsyncClient() + _RecordingClient.closed = _RecordingAsyncClient.closed = False + + with _owned(You(api_key_auth="k", client=sync_client, async_client=async_client)): + pass + + assert _RecordingClient.closed + assert _RecordingAsyncClient.closed + + def test_drops_references(self): + with _owned(You(api_key_auth="k", client=httpx.Client())) as you: + pass + assert you.sdk_configuration.client is None + assert you.sdk_configuration.async_client is None + + def test_supplied_clients_are_not_closed(self): + sync_client = _RecordingClient() + _RecordingClient.closed = False + + # client_supplied stays True — the caller owns this transport. + with You(api_key_auth="k", client=sync_client): + pass + + assert not _RecordingClient.closed + sync_client.close() + + def test_async_close_failure_does_not_mask_body_exception(self): + you = _owned( + You(api_key_auth="k", client=httpx.Client(), async_client=_BoomAsyncClient()) + ) + with pytest.raises(ValueError, match="the real error"): + with you: + raise ValueError("the real error") + + def test_sync_close_failure_does_not_mask_body_exception(self): + you = _owned(You(api_key_auth="k", client=_BoomClient())) + with pytest.raises(ValueError, match="the real error"): + with you: + raise ValueError("the real error") + + def test_close_failure_does_not_raise_on_clean_exit(self): + you = _owned(You(api_key_auth="k", client=_BoomClient())) + with you: + pass # must not raise + + +class TestAsyncExit: + @pytest.mark.asyncio + async def test_closes_both_clients(self): + sync_client = _RecordingClient() + async_client = _RecordingAsyncClient() + _RecordingClient.closed = _RecordingAsyncClient.closed = False + + async with _owned( + You(api_key_auth="k", client=sync_client, async_client=async_client) + ): + pass + + assert _RecordingClient.closed + assert _RecordingAsyncClient.closed + + @pytest.mark.asyncio + async def test_drops_references(self): + async with _owned(You(api_key_auth="k", async_client=httpx.AsyncClient())) as you: + pass + assert you.sdk_configuration.client is None + assert you.sdk_configuration.async_client is None + + @pytest.mark.asyncio + async def test_close_failure_does_not_mask_body_exception(self): + you = _owned(You(api_key_auth="k", async_client=_BoomAsyncClient())) + with pytest.raises(ValueError, match="the real error"): + async with you: + raise ValueError("the real error") + + @pytest.mark.asyncio + async def test_sync_exit_inside_running_loop_does_not_raise(self): + """`with You(...)` used from async code must not trip over the live loop.""" + with _owned(You(api_key_auth="k", async_client=httpx.AsyncClient())): + pass diff --git a/tests/test_live.py b/tests/test_live.py index 88658d2..9602415 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -642,7 +642,7 @@ def test_frontier_without_background_raises_422(self, you_client): # --------------------------------------------------------------------------- -# Answer API (new in 2.6.0) +# Answer API (new in 3.0.0) # --------------------------------------------------------------------------- @requires_api_key class TestLiveAnswer: diff --git a/tests/test_param_normalization.py b/tests/test_param_normalization.py new file mode 100644 index 0000000..5e8a807 --- /dev/null +++ b/tests/test_param_normalization.py @@ -0,0 +1,191 @@ +"""Tests for plain-string parameter normalization. + +The SDK advertises that enum-typed parameters accept plain strings in any +case so callers never have to import an enum class. `country`/`language` +normalize upward (their enum members are uppercase); `safesearch`, +`livecrawl`, `livecrawl_formats`, and `freshness` normalize downward. + +Also pins the three-way `language` contract, which is easy to break: + + omitted -> API default ("EN") + explicit None -> field omitted entirely (no language filter) + "en" / "EN" -> "EN" +""" + +import json + +import httpx +import pytest + +from youdotcom import You +from youdotcom.models import Country, Language, LiveCrawl, SafeSearch + + +_SEARCH_BODY = json.dumps({"results": {"web": []}}) +_ANSWER_BODY = json.dumps({"answer": "hi", "citations": [], "results": {"web": []}}) + + +def _search_body(**kwargs) -> dict: + """Run one search and return the JSON body that went over the wire.""" + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + ) + + with You( + api_key_auth="k", + server_url="http://mock.local", + client=httpx.Client(transport=httpx.MockTransport(handler)), + ) as you: + you.search(query="x", **kwargs) + return captured["body"] + + +def _answer_body(**kwargs) -> dict: + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_ANSWER_BODY + ) + + with You( + api_key_auth="k", + server_url="http://mock.local", + client=httpx.Client(transport=httpx.MockTransport(handler)), + ) as you: + you.answer(query="x", **kwargs) + return captured["body"] + + +class TestUppercaseParams: + @pytest.mark.parametrize("value", ["us", "US", "uS", Country.US]) + def test_country_normalizes_to_upper(self, value): + assert _search_body(country=value)["country"] == "US" + + @pytest.mark.parametrize("value", ["en", "EN", Language.EN]) + def test_language_normalizes_to_upper(self, value): + assert _search_body(language=value)["language"] == "EN" + + def test_hyphenated_language_normalizes(self): + assert _search_body(language="zh-hans")["language"] == "ZH-HANS" + + +class TestLowercaseParams: + @pytest.mark.parametrize("value", ["strict", "STRICT", SafeSearch.STRICT]) + def test_safesearch_normalizes_to_lower(self, value): + assert _search_body(safesearch=value)["safesearch"] == "strict" + + @pytest.mark.parametrize("value", ["web", "WEB", LiveCrawl.WEB]) + def test_livecrawl_normalizes_to_lower(self, value): + assert _search_body(livecrawl=value)["livecrawl"] == "web" + + def test_livecrawl_formats_normalize_each_item(self): + body = _search_body(livecrawl="web", livecrawl_formats=["HTML", "Markdown"]) + assert body["livecrawl_formats"] == ["html", "markdown"] + + @pytest.mark.parametrize("value", ["week", "WEEK", "Week"]) + def test_freshness_keyword_normalizes_to_lower(self, value): + assert _search_body(freshness=value)["freshness"] == "week" + + def test_freshness_date_range_separator_normalizes(self): + """`YYYY-MM-DDtoYYYY-MM-DD` needs a lowercase `to`; uppercase input is fixed up.""" + body = _search_body(freshness="2026-01-01TO2026-02-01") + assert body["freshness"] == "2026-01-01to2026-02-01" + + def test_freshness_date_range_unchanged_when_already_lowercase(self): + body = _search_body(freshness="2026-01-01to2026-02-01") + assert body["freshness"] == "2026-01-01to2026-02-01" + + +class TestLanguageThreeWayContract: + def test_omitted_uses_api_default(self): + assert _search_body()["language"] == "EN" + + def test_explicit_none_sends_no_language(self): + """Passing None must opt out of the filter, not fall back to the default.""" + assert "language" not in _search_body(language=None) + + def test_explicit_value_wins(self): + assert _search_body(language="fr")["language"] == "FR" + + @pytest.mark.asyncio + async def test_async_matches_sync(self): + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + ) + + async with You( + api_key_auth="k", + server_url="http://mock.local", + async_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), + ) as you: + await you.search_async(query="x", language=None) + assert "language" not in captured["body"] + + +class TestAnswerNormalization: + def test_country_and_language_upper(self): + body = _answer_body(country="us", language="en") + assert body["country"] == "US" + assert body["language"] == "EN" + + def test_freshness_lower(self): + assert _answer_body(freshness="MONTH")["freshness"] == "month" + + def test_optional_params_omitted_when_unset(self): + body = _answer_body() + for field in ("country", "language", "freshness"): + assert field not in body + + +class TestDeprecatedShimNormalization: + """The deprecated `unified()` spelling must normalize identically.""" + + def test_unified_normalizes_and_defaults_language(self): + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + ) + + with You( + api_key_auth="k", + server_url="http://mock.local", + client=httpx.Client(transport=httpx.MockTransport(handler)), + ) as you: + with pytest.warns(DeprecationWarning): + you.search.unified(query="x", country="us", safesearch="STRICT") + + assert captured["body"]["country"] == "US" + assert captured["body"]["safesearch"] == "strict" + assert captured["body"]["language"] == "EN" + + def test_unified_language_none_opts_out(self): + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + ) + + with You( + api_key_auth="k", + server_url="http://mock.local", + client=httpx.Client(transport=httpx.MockTransport(handler)), + ) as you: + with pytest.warns(DeprecationWarning): + you.search.unified(query="x", language=None) + + assert "language" not in captured["body"] diff --git a/tests/test_redaction.py b/tests/test_redaction.py new file mode 100644 index 0000000..9a2d6c1 --- /dev/null +++ b/tests/test_redaction.py @@ -0,0 +1,113 @@ +"""Tests for debug-log header redaction. + +`BaseSDK` logs full request/response headers when a debug logger is attached. +`_redact_headers` keeps credentials out of those logs. These tests exist so a +future edit that passes `req.headers` straight to the logger fails CI rather +than silently leaking API keys into a customer's log aggregator. +""" + +import json +import logging + +import httpx +import pytest + +from youdotcom import You +from youdotcom.basesdk import _redact_headers + + +_SEARCH_BODY = json.dumps({"results": {"web": []}}) + +API_KEY = "sk-super-secret-key" + + +def _run_with_debug_log(**client_kwargs) -> str: + """Issue one search with debug logging on and return everything logged.""" + records: list[str] = [] + + class _Capture(logging.Handler): + def emit(self, record): + # getMessage() already interpolates record.args. + records.append(record.getMessage()) + + logger = logging.getLogger("youdotcom.test_redaction") + logger.handlers = [] + logger.setLevel(logging.DEBUG) + logger.propagate = False + logger.addHandler(_Capture()) + + def handler(request): + return httpx.Response( + 200, + headers={ + "content-type": "application/json", + "set-cookie": "session=super-secret-cookie", + }, + content=_SEARCH_BODY, + ) + + with You( + api_key_auth=API_KEY, + server_url="http://mock.local", + client=httpx.Client(transport=httpx.MockTransport(handler)), + debug_logger=logger, + **client_kwargs, + ) as you: + you.search(query="x") + + return "\n".join(records) + + +class TestRedactHeaders: + """Unit-level behavior of the helper itself.""" + + @pytest.mark.parametrize( + "name", + ["Authorization", "X-API-Key", "Cookie", "Set-Cookie"], + ) + def test_sensitive_headers_are_redacted(self, name): + redacted = _redact_headers(httpx.Headers({name: "secret-value"})) + assert redacted[name] == "[REDACTED]" + assert "secret-value" not in str(redacted) + + @pytest.mark.parametrize("name", ["x-api-key", "X-API-KEY", "AuThOrIzAtIoN"]) + def test_matching_is_case_insensitive(self, name): + assert _redact_headers(httpx.Headers({name: "secret"}))[name] == "[REDACTED]" + + def test_non_sensitive_headers_pass_through(self): + redacted = _redact_headers( + httpx.Headers({"Content-Type": "application/json", "User-Agent": "ua/1.0"}) + ) + assert redacted["Content-Type"] == "application/json" + assert redacted["User-Agent"] == "ua/1.0" + + def test_original_headers_are_not_mutated(self): + original = httpx.Headers({"X-API-Key": "secret"}) + _redact_headers(original) + assert original["X-API-Key"] == "secret" + + def test_repeated_sensitive_header_is_fully_redacted(self): + headers = httpx.Headers( + [("Set-Cookie", "a=first"), ("Set-Cookie", "b=second")] + ) + redacted = str(_redact_headers(headers)) + assert "first" not in redacted and "second" not in redacted + + +class TestDebugLogRedaction: + """End-to-end: the key never reaches the debug log.""" + + def test_api_key_not_logged(self): + assert API_KEY not in _run_with_debug_log() + + def test_redaction_marker_present(self): + assert "[REDACTED]" in _run_with_debug_log() + + def test_response_set_cookie_not_logged(self): + assert "super-secret-cookie" not in _run_with_debug_log() + + def test_useful_context_still_logged(self): + """Redaction must not gut the log — the request line still has to be there.""" + logged = _run_with_debug_log() + assert "/v1/search" in logged + assert "POST" in logged diff --git a/tests/test_security_env.py b/tests/test_security_env.py index d40b0ee..b4f67fa 100644 --- a/tests/test_security_env.py +++ b/tests/test_security_env.py @@ -1,15 +1,23 @@ -"""Tests for the hand-applied env-var precedence in `get_security_from_env`. +"""Tests for API-key resolution. `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 accidental regressions are caught by CI immediately. + +`TestConstructorKeyResolution` covers the constructor side: how +`You(api_key_auth=...)` interacts with that env fallback. `TestEmptyKeyRejected` +pins the rejection of empty keys, which is what keeps a missing key from +silently resolving to a different identity. """ +import json import os +import httpx import pytest +from youdotcom import You from youdotcom.models import Security from youdotcom.utils.security import get_security_from_env @@ -53,3 +61,88 @@ def test_explicit_security_overrides_env(monkeypatch): assert result is not None assert result.api_key_auth == "explicit-key" + + +_SEARCH_BODY = json.dumps({"results": {"web": []}}) + + +def _sent_api_key(**client_kwargs) -> str | None: + """Run one search through a mock transport and report the X-API-Key sent.""" + captured: dict = {} + + def handler(request): + captured["key"] = request.headers.get("x-api-key") + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + ) + + with You( + server_url="http://mock.local", + client=httpx.Client(transport=httpx.MockTransport(handler)), + **client_kwargs, + ) as you: + you.search(query="x") + return captured["key"] + + +class TestConstructorKeyResolution: + """`You(api_key_auth=...)` vs. the env-var fallback.""" + + def test_explicit_key_is_sent(self): + assert _sent_api_key(api_key_auth="explicit-key") == "explicit-key" + + def test_none_falls_back_to_env(self, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "env-key") + assert _sent_api_key(api_key_auth=None) == "env-key" + + def test_omitted_falls_back_to_legacy_env(self, monkeypatch): + monkeypatch.setenv("YOU_API_KEY_AUTH", "legacy-key") + assert _sent_api_key() == "legacy-key" + + def test_callable_key_is_sent(self): + assert _sent_api_key(api_key_auth=lambda: "from-callable") == "from-callable" + + +class TestEmptyKeyRejected: + """An empty key is a caller mistake, and is rejected where it happens. + + Every endpoint requires a key, so `""` is never a valid argument — it + means someone believed they were passing a key and weren't, nearly always + `os.getenv("YDC_API_KEY", "")` with the variable unset. Falling back to + the environment there would run the request under whatever identity the + environment holds rather than the one the code asked for, so the SDK + raises instead. + """ + + def test_empty_string_raises_at_construction(self): + with pytest.raises(ValueError, match="api_key_auth was an empty string"): + You(api_key_auth="") + + @pytest.mark.parametrize("value", ["", " ", "\n", "\t "]) + def test_blank_strings_all_rejected(self, value): + with pytest.raises(ValueError): + You(api_key_auth=value) + + def test_raises_even_when_env_is_set(self, monkeypatch): + """The environment must not paper over the mistake.""" + monkeypatch.setenv("YDC_API_KEY", "env-key") + monkeypatch.setenv("YOU_API_KEY_AUTH", "legacy-key") + with pytest.raises(ValueError): + You(api_key_auth="") + + def test_message_points_at_the_likely_cause(self): + with pytest.raises(ValueError) as excinfo: + You(api_key_auth="") + message = str(excinfo.value) + assert 'os.getenv("YDC_API_KEY")' in message + assert "pass None or omit the argument" in message + + def test_callable_returning_empty_raises_when_called(self): + """A callable is resolved lazily, so it can only be checked on use.""" + you = You(api_key_auth=lambda: "") # construction is fine + with pytest.raises(ValueError, match="callable returned an empty API key"): + you.search(query="x") + + def test_none_is_still_the_way_to_use_the_environment(self, monkeypatch): + monkeypatch.setenv("YDC_API_KEY", "env-key") + assert _sent_api_key(api_key_auth=None) == "env-key" diff --git a/uv.lock b/uv.lock index ecb1231..3ca2536 100644 --- a/uv.lock +++ b/uv.lock @@ -111,6 +111,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.15.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/45/78dbf9604ee5b3db24efbf26bed1cb58862fb40480cba821963c69348751/coverage-7.15.3.tar.gz", hash = "sha256:ae7ea5a4614acf399ef0483c4cb34f8f8f01df848d8fcbe7d3ce0865733f1c4d", size = 935592, upload-time = "2026-08-02T18:50:17.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/d9/01d8e19b2c0e55903bfb540c9f6bd32326f1d5b2fcb5a7dd8648ae2dd9c5/coverage-7.15.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3a82b2ceee91ba353e59fe2436d8a9eae799ff9825e5385423ea205d693e2949", size = 222202, upload-time = "2026-08-02T18:47:25.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/92/1c23aeb83c7239af07061abc6e96f00f9b62deec8fae022cab1b353e6d46/coverage-7.15.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3088cce65e54c2eefc08e7e1ca0b0acec1e95e8cf084ac848599103ed0367f74", size = 222723, upload-time = "2026-08-02T18:47:28.359Z" }, + { url = "https://files.pythonhosted.org/packages/b9/76/186f60bae815941553b70877d814c45994db8198bb76933bd062c18ee437/coverage-7.15.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a65e09efb0b5ab21fc54a8a65c5b2e533c0a4c0d064af0259a005dc656dc1b13", size = 249461, upload-time = "2026-08-02T18:47:29.802Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/53e010accfea3340905c5bb9207a2e461bba9b372621f1b88c1bd0e1392a/coverage-7.15.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b51f279a2477b0e1f288b98f141fd227acfdd1d3f0370400e473788879b47871", size = 251290, upload-time = "2026-08-02T18:47:31.445Z" }, + { url = "https://files.pythonhosted.org/packages/5b/13/d916056137fb6969e9d9f58ee11d1ef56778673843828315030733a3a0b6/coverage-7.15.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7835176988cbcf1f014db683bc33aa15e0558e412bf08deaa99757335b88df15", size = 253156, upload-time = "2026-08-02T18:47:33.033Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/0a4198d82e765f3351a91714d42526eb765e5c97776f7674489acbe7d062/coverage-7.15.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f24896dc8863167f6732f4142f5d37e6195eccc8fe5fe528d35d49597d29fdb3", size = 255068, upload-time = "2026-08-02T18:47:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/ebe4e0751e3637d87162887d0d3cdf4716f96782ab6face09a295e74cba4/coverage-7.15.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9490d43e5d041fdf376770a886a29722adb05f6b9c21a65c48c81fc8f1c33fd7", size = 250142, upload-time = "2026-08-02T18:47:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a2/12977c74fcf92f9b1da45fb9576c5f593a2c47bde04e937a8bb32dd56bfa/coverage-7.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:225e359bd5dedaff6d68e36091af20555866c557d968167308b677379bf575c3", size = 251195, upload-time = "2026-08-02T18:47:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c8/42bd9aa40386c0fbcc7af221ab4737dad10d27bb4971ca2783676619a79b/coverage-7.15.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:22119e2e3b2ac5ac024d50131fdd4b22ab4c6cf8aa2fc792cce73c0d94c5812d", size = 249200, upload-time = "2026-08-02T18:47:39.773Z" }, + { url = "https://files.pythonhosted.org/packages/0a/52/f1ce0dd8a2ec5c3911f1bc98b859be09cc4bbd705ed74ceb905729837c79/coverage-7.15.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:12d555badc462b0f6037ce8bec8b4af8d71f90eb55b57d0a358731f7ee7883e2", size = 253013, upload-time = "2026-08-02T18:47:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2c/9a642c4cf7b6992b2eba75359b6cb548bd437001d6083fd0ffe492b80d38/coverage-7.15.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c4e2cf9cf774939b3dc581c6e31dfe7e8d7608b24f0f17524d6161f8235c3d2c", size = 249470, upload-time = "2026-08-02T18:47:42.997Z" }, + { url = "https://files.pythonhosted.org/packages/89/32/271d85639ac5de099046f7418e850047b1e964f893535128997b5cddde8d/coverage-7.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cea1b3e19d710f67e2ba9ce0b0b51032c2a9b4808a65ced48ddf336ef7e58058", size = 250073, upload-time = "2026-08-02T18:47:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/15/71/6216430095c5437f83d7bfa7c1adb0965e26ada88d9fff49bf55e2cab154/coverage-7.15.3-cp310-cp310-win32.whl", hash = "sha256:25c77560309f157e7b7ee8fe0bf78d047ba900b7ae42f0e50e559305b366fea2", size = 224263, upload-time = "2026-08-02T18:47:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/8a/f1032fb2714c28fedf00d73562c4bb9f713fa8a90593ed577bbb708a7de1/coverage-7.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:179fbf847e6c3d90ea71bfd570fe57f1ddb1c51474754894871c1e11099efaa0", size = 224886, upload-time = "2026-08-02T18:47:47.668Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9c/c8a3a923c24f631695cea2d5e2f02e776bc0af6e03800626e13a6c05a615/coverage-7.15.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5f3f854ab4599d98f7799ac9b91e34e8ec9ebc9a6372ee8c1f3413a68cc8b5e9", size = 222328, upload-time = "2026-08-02T18:47:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/92/51/dda77f34cbd2513d6ffb898c901d19e9ca55f48c0cbc4a1eb173a97d157a/coverage-7.15.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75268348fee1f199653b8a846262aec5581c6bb008c4f58824959fb708cc688f", size = 222832, upload-time = "2026-08-02T18:47:51.219Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/e0faafc4c6e23bd76c76148875ee9ec5781b8f1cd62cea2bc4ca0f0f0e5d/coverage-7.15.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21081739f6264cc594cad2d42b62befbd17633824022866c68720eb0c4b8d6b4", size = 253250, upload-time = "2026-08-02T18:47:52.737Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/4b1e0eeb727ffb471e411c1bd3402184b5dd54a77a762b0e55e87cdf9ae3/coverage-7.15.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:718d366251b060c10731c7dd359de6caea72250036eb94576aa56dacbf830a11", size = 255160, upload-time = "2026-08-02T18:47:54.404Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/a602d2d48f9db9f795e578a86aa914f7b20008e9330902defcfb73d17b3a/coverage-7.15.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa1bbaa502a6e877f3ee67cbac3eba2bb637f623e454e6c37b81b38896dbd48f", size = 257269, upload-time = "2026-08-02T18:47:56.157Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/bf6db13df2fcee00d2671849fe58c99232ee79a01fec7478c2bf7839b9e1/coverage-7.15.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:494880c9e60782610683f4eb9b65cce4f886673596b8f3cb2dfa079fc551c743", size = 259231, upload-time = "2026-08-02T18:47:57.76Z" }, + { url = "https://files.pythonhosted.org/packages/89/37/8118f13b17fa7d9a3aa2c301d93f2d5ffeef70fa7e27e639a74bdacd3fea/coverage-7.15.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3db264ea689f9e8f9fa4fb9005fee4048c3bff4a547f4cfa27f5086cb0804ec0", size = 253357, upload-time = "2026-08-02T18:47:59.261Z" }, + { url = "https://files.pythonhosted.org/packages/97/6d/c7b94fb03962f4d6f0fe13d01c4eb9c4c6e2e714a20d074516ec7582b110/coverage-7.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e869d4799674d67778e76ddbe2e26cf1673369262e231a8ec259421b1015fea", size = 254961, upload-time = "2026-08-02T18:48:00.901Z" }, + { url = "https://files.pythonhosted.org/packages/87/f9/fe0bd415fa56e36b62b649017c8fc98330858be4c7593789efb78cd24178/coverage-7.15.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:696fc7a28bbf717aba8d2c6963d26702945c7832cb313ba3b323aa5b1afb3156", size = 253024, upload-time = "2026-08-02T18:48:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7c/ffa53506d63ba8a77f5b9557dd6f5a5a5ad85adc680d7857410138f82bd9/coverage-7.15.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3fe9be1c527497d047f770d88a0110189714c36383bb88384508f750c302bffa", size = 256792, upload-time = "2026-08-02T18:48:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c6/df42458e72c18a49fe87e40ccd3fb0314210915256cf4a5593e1b3250e04/coverage-7.15.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2400591f4b2e33746c70846388f8bb4c7e33b820e31cb8c6cb2f25305310438b", size = 252744, upload-time = "2026-08-02T18:48:06.154Z" }, + { url = "https://files.pythonhosted.org/packages/f1/14/8bf18a4b10a44f8ba5f604b00e102f37daf49d581d66a37dc33fa267e1a6/coverage-7.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e557178799282269412a672e5753f2179edfe1b3f0f19b0c98f8e72d482326a", size = 253652, upload-time = "2026-08-02T18:48:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/e530c9bb94e4155817cbd149034105b062a6913bc356ae08f454d155de53/coverage-7.15.3-cp311-cp311-win32.whl", hash = "sha256:68ea6c947375982ae907e19e9d2ef156bd6e68e11f3566dd568d7f4ec974e715", size = 224428, upload-time = "2026-08-02T18:48:09.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/98/0050c692d120988f1973a15196f52dee4ae221848b760281461a2005b613/coverage-7.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:28743dad31622e8c474b17446118037361f5b1f4f2ecdf72d4f6fde246d64446", size = 224906, upload-time = "2026-08-02T18:48:11.611Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ae/c0ef3e2ba3f35fc1c6985811a40edd9331e5b8978c9ecf84699de3edacbe/coverage-7.15.3-cp311-cp311-win_arm64.whl", hash = "sha256:c4398918c4fda32718191239e451fd86ac5ad1e8979b592f1921ee2d1f038965", size = 224448, upload-time = "2026-08-02T18:48:13.304Z" }, + { url = "https://files.pythonhosted.org/packages/d1/6c/bac99d9d4c6abe856e93bf3f5212982ac0bfac126dd4a042753bd53bc5af/coverage-7.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79a3e32e83227d83d9684459ed579769b56c369ac2d7313099b2d9e031d2e10f", size = 222499, upload-time = "2026-08-02T18:48:15.018Z" }, + { url = "https://files.pythonhosted.org/packages/aa/bc/cb9a39b083bc1aa70586482dab25c9be20bab0ec6c155340e50d9066bb1e/coverage-7.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:767feb87c5886d781d0a69fafd450a20826ddab7b79bce1665deb64d21441b60", size = 222866, upload-time = "2026-08-02T18:48:16.884Z" }, + { url = "https://files.pythonhosted.org/packages/58/fb/beaa453d62000a0a5b39838bee2a137afe609a50a71f55e83c73461e513b/coverage-7.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50951e37033c40548d777b8a8454a2cd622dba1136780065678dccaec307c47f", size = 254367, upload-time = "2026-08-02T18:48:18.507Z" }, + { url = "https://files.pythonhosted.org/packages/66/64/43e72500ed6815cef189f9193f29d7af4b078830337c95ea976cd0c0d427/coverage-7.15.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:63a4ff67364afb2cac826b8bbd78a5c50ce656a7b7137436b44d7b96a9271088", size = 257103, upload-time = "2026-08-02T18:48:20.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/2893e2937adfe02f45fd38e4a8a0a0d8b7a02ff9e012ac3d009bee3c4f16/coverage-7.15.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95e42856509675fe26560310313a6117640e96f9a1e19bb3d220116a27c94c", size = 258220, upload-time = "2026-08-02T18:48:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d5e6e2eb1a62961083734291304b1f85df72e2abe95c76eb88a7f472afd0/coverage-7.15.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abad631cba27094b4631993f4c72e89ac0ca1b3a0236c7abaf8ca79aea619851", size = 260481, upload-time = "2026-08-02T18:48:23.682Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/9b72c5c6a9798a9a12cf65f66e077cc1fdd396e61915c862688f9afe1cae/coverage-7.15.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b0807f1f051dd82a234ad6acdb6f1425baede60be1e84e862496c8cc9262ab9", size = 254749, upload-time = "2026-08-02T18:48:25.32Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/e1c2f759e2dbce559ba85c40c0e4acfecc6cff4b740c294c88e41ccc6111/coverage-7.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8d6df7aeb5bc464040bbc9ae173d875785d3677ebc4307817997d622d74225e", size = 256138, upload-time = "2026-08-02T18:48:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ab/48cc7e760f769e86ae290a125ea6e7209dfbdbbbb7ff4f5d9d1ee7a45d57/coverage-7.15.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:974471c506c9f5758808b47c1ebf7949ecd0848f5c1020e78675fefe5ff46866", size = 254283, upload-time = "2026-08-02T18:48:29.082Z" }, + { url = "https://files.pythonhosted.org/packages/15/26/39529a68154f99b3a1829debd8b25eac384effeec890a293b5bbdcb49186/coverage-7.15.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cba0c9c13e35c86df7998f1afaf6b1da224a3a39e4da59bdabf60c148046dcb", size = 258352, upload-time = "2026-08-02T18:48:30.892Z" }, + { url = "https://files.pythonhosted.org/packages/91/2f/55b82aa3d8d7dd8023a56e7c5c2a70e39a3c44b3353c6cf3faec9ad51566/coverage-7.15.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4d608dc36a364dce33acbf4fc3a50f9d2054c945f233bb0a2cdb4b90bfa17646", size = 253852, upload-time = "2026-08-02T18:48:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6d/839f4045124cd3518ecf2c58967e58a911202834e7c5a03cfdf2ab0b29f6/coverage-7.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2395869280554a1941da904423c12660c39f721315e1c02d076a7fe0971382f0", size = 255725, upload-time = "2026-08-02T18:48:34.848Z" }, + { url = "https://files.pythonhosted.org/packages/75/21/d25e3e2a9e327798078c877f469dfb6def860bf6e25036529046227d3e15/coverage-7.15.3-cp312-cp312-win32.whl", hash = "sha256:24f3b21840c3eb76cef3cc70b2bf6649010c64471a84a446538a39306e1ba04d", size = 224566, upload-time = "2026-08-02T18:48:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/df90cc1e8d095ce263968a93e04829821b2afb31ac2752c06a2e0a8e3c13/coverage-7.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:fa7b17902c3c1dd8a7adb52679b7f6340bba08443d710c8838e04db8cf62be2a", size = 225098, upload-time = "2026-08-02T18:48:38.941Z" }, + { url = "https://files.pythonhosted.org/packages/65/c7/ec49e43c58967a07163e2d1c6bbd58112b825b2772ab66784afd6a5400ba/coverage-7.15.3-cp312-cp312-win_arm64.whl", hash = "sha256:fcbe83fb7258eacd293bf5322d88807acb35ed12a5cfa99dd8215c083e3b0235", size = 224485, upload-time = "2026-08-02T18:48:40.682Z" }, + { url = "https://files.pythonhosted.org/packages/68/6e/62ae61e1fc434956bec38ed1d5b1c494f58cf579dbd998e77abffe7b3e6b/coverage-7.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1182eed05674c63d40951fae27c43e822749f04d25f75df64c2e4fa3168678de", size = 222522, upload-time = "2026-08-02T18:48:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/13/ff/c74c673d81e0e77b6608c3d21331e3db42e30daeb3c8a0a8860d4c9e2e14/coverage-7.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c0c4b0d7c4cd56e470d0c9d8441f42e8a96cdfd95050fec027f1d4dd9f11006c", size = 222894, upload-time = "2026-08-02T18:48:44.274Z" }, + { url = "https://files.pythonhosted.org/packages/a1/91/ccb30f5ffafd7d69d0b18e5162f9b711a5654e807b7b0c13497f0826b33f/coverage-7.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5c9fce9f4998b0d50a753da765b9215a14decc7863822c89d72da7a89ca625b3", size = 253890, upload-time = "2026-08-02T18:48:46.097Z" }, + { url = "https://files.pythonhosted.org/packages/29/c6/e92a66cda49a2751b09826d51258f199b92aa0cb005bc5f34e9729a52a9c/coverage-7.15.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a47e2a0a0ace9241e70ee00e44520f88b843094603dd54303f1bafecd929c30", size = 256484, upload-time = "2026-08-02T18:48:47.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/730929164b457cf25cf76c23898b90f9039a104a647890801b6586797b14/coverage-7.15.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95bad94f83807ae60ed76f3ac012f69b2605ac9ea81bee959a5a483f7fa09c10", size = 257723, upload-time = "2026-08-02T18:48:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/9e/be/04cb5672cb19f5c389eda81ba22d89807699a949653d3625b0e0fda169da/coverage-7.15.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:228e172a76c428bb17d1ab78a2ff188990b0597e5dbd291f52a4edf7412de049", size = 259854, upload-time = "2026-08-02T18:48:51.413Z" }, + { url = "https://files.pythonhosted.org/packages/96/25/5e7fd6af39f6507071455944b8906dd1fe5b7b6bffb6a163ceb20afa0d13/coverage-7.15.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cea9fb33887c99349996266f1fd60abe5af3577a90633392001d27ef46b4b66e", size = 254085, upload-time = "2026-08-02T18:48:53.158Z" }, + { url = "https://files.pythonhosted.org/packages/23/c8/55e58a853f1e61163a6e755897bd14a059d78411e86560f39d9951c019b5/coverage-7.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81760de3155d7f52c21860c4046628dc6bed182f72e3c028e2b4fd46f65aa040", size = 255850, upload-time = "2026-08-02T18:48:55.031Z" }, + { url = "https://files.pythonhosted.org/packages/be/74/8bcec66dbcf3d22bea2a0b2b77ee2fa6f766a647d0023d4eabbc4f2b2756/coverage-7.15.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b47ea0a1d3a3d089826c6cbfad8429d7d8872e28e86baa95ddef330f6875da21", size = 253818, upload-time = "2026-08-02T18:48:57.163Z" }, + { url = "https://files.pythonhosted.org/packages/ce/06/450b673fdfece0997b4e16a31d6bde6b18889c578f1013ddd34c962ac6f9/coverage-7.15.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5459ba486b2a5d58a6c05254779ecdf525e7f20174d0210ceda75ba40fdb8f2c", size = 257973, upload-time = "2026-08-02T18:48:59.098Z" }, + { url = "https://files.pythonhosted.org/packages/56/fd/3ec7409aec0ddc943132452b65672f065f043b844f1830e1fe173c98b3ab/coverage-7.15.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c59209f80a08dbfcdd5109a80dc623cd3b9d22895c85757d34f57a6e6e95570f", size = 253638, upload-time = "2026-08-02T18:49:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/75/20/30a8dabb194123631c93f860fdd86401ad405d56cfb1841873afbfe4e92b/coverage-7.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f863856c1779d4a5bb6a94698a2f9073e09c6706501f76f3e7780e72df97d21c", size = 255407, upload-time = "2026-08-02T18:49:03.143Z" }, + { url = "https://files.pythonhosted.org/packages/13/4d/e14365b1953b43653341412f9088b0d752614c626a73a705ff9af400f3a3/coverage-7.15.3-cp313-cp313-win32.whl", hash = "sha256:00cbdc5e322927dc30c5e42b863819b1bb867cc66f26ab5372c585850876ab93", size = 224575, upload-time = "2026-08-02T18:49:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/1c/64/88f762ea80de2070207246faef514513be874486b2773528f2cc2b4b515c/coverage-7.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:835528518a1d823cf336740324b2f335f7c01e609e74abcb5d5163b3e66661e3", size = 225116, upload-time = "2026-08-02T18:49:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/ab/66/03c34c53a319f522554cd29d4f2e16c5eab61aa4cdcf55753129fd7d926c/coverage-7.15.3-cp313-cp313-win_arm64.whl", hash = "sha256:0d2e1f2cbbf36b842f3e2aff8d118c60d677adb498bc6c7fa9c6838738f82767", size = 224509, upload-time = "2026-08-02T18:49:09.129Z" }, + { url = "https://files.pythonhosted.org/packages/35/6f/8c2dc014357618b3226c90f731b8282766c3685786f422558991dc49fbf2/coverage-7.15.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1e3bb08ad574bd9fb6a991f645728f70d333c1c1958dd5fcde65e24cb862813d", size = 222571, upload-time = "2026-08-02T18:49:11.242Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/d867c7ceae9d56b7e74ee61ea834f1aa4f9a1e1c7f0ce39393ba573b1c12/coverage-7.15.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e5860eaff02a0b7f1b73304bdf846596ee62ab3a78d25c68044ebf684cb1fef", size = 222902, upload-time = "2026-08-02T18:49:13.448Z" }, + { url = "https://files.pythonhosted.org/packages/62/77/4f6dfc490c5f2bcacb2d296d9aa4d1e128c43b48e94ad313fec7f49f09ad/coverage-7.15.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:60874e5bd67f0b1bdbe42ab42c7bafa66a6fb8de88721af6df3f7a02713960cd", size = 253947, upload-time = "2026-08-02T18:49:15.304Z" }, + { url = "https://files.pythonhosted.org/packages/16/8a/6777f192af264165103e2a3d3768dbadb9894a0a2359a16877141d9ae8f5/coverage-7.15.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9147be876e9d83765e0b82176674dc248a6b9283e25e01e7462611b97e9b731", size = 256452, upload-time = "2026-08-02T18:49:17.801Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7b/3d7ac46a0234bc684f41ee42be95e29b2b6525695adb04083609d5ac2149/coverage-7.15.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61a01f8c3804760fcc5a3d31c4f3cab792d660d44e17bf7adeaf0ea51e07821e", size = 257798, upload-time = "2026-08-02T18:49:19.878Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/c6ee59c29afcb5fdb35f936381340d1a06429a07c48f20e809646647acbe/coverage-7.15.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95bf3e7f26f792e25eb185f85a5a659d48479265176dcfe22b6f334fd0081b5c", size = 260112, upload-time = "2026-08-02T18:49:21.858Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e1/e8ea39a46e89e3a143312ee5f80336e992e3ae8fe44bf9c76b83fefeed42/coverage-7.15.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44c41eff9e413fed8740eca75d5438ebeb9d3e45e7cd37c67329213e7a72c764", size = 253944, upload-time = "2026-08-02T18:49:23.926Z" }, + { url = "https://files.pythonhosted.org/packages/95/67/31ab5f6a37fd887d1386f81f0da9306851ad2264e9baaa9c7f606e0b3e17/coverage-7.15.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54146bafb61f3ba9895b43af0dd17eba01561d586d44ce84ea221b0cbbee5a9e", size = 255805, upload-time = "2026-08-02T18:49:25.973Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6a/ee505a80c8fd89620fb337c0596daecff87f33171fbb4ee3015fc3d7331f/coverage-7.15.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:af000dd1bb859ff8066fda4c79512ff938c798116540307226b373099c7b151f", size = 253769, upload-time = "2026-08-02T18:49:27.883Z" }, + { url = "https://files.pythonhosted.org/packages/b0/41/6ab0f81c9e89660230d8f3f581d4732e5ddb75a885b0a5dfc73d315dc94f/coverage-7.15.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a1b82490577f3889950b5a04f18712aef0207243e0749d60fe28c3c73ebfd5fd", size = 258045, upload-time = "2026-08-02T18:49:30.201Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/c995e91cae28cf31d6defab3bfb553dda5ac83ac7381b0f2b121264c307a/coverage-7.15.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c4fc90a60154c3e4b8a2dc206d6dbe852f1c235c249e0dc0cef909d032c9591a", size = 253587, upload-time = "2026-08-02T18:49:32.349Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/f2049980f82d6890321f2065f9e66216eabbf4b2001815db958bc543f40a/coverage-7.15.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f25bb884814a892948b4c20394db3f2364dd452d9492736479e7a493e63b0eb6", size = 255243, upload-time = "2026-08-02T18:49:34.324Z" }, + { url = "https://files.pythonhosted.org/packages/1d/82/2c841b67a978c0eb9c3707630b68f93f9e7585d78bb906bc8823ec6b07a5/coverage-7.15.3-cp314-cp314-win32.whl", hash = "sha256:722dbf8e7828fbcfe0dc8586167dc0a5ce85ad6ea171dbb21ed3f8d6581d3cb8", size = 224759, upload-time = "2026-08-02T18:49:36.326Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/5c93ec43784fd3e404ca23cd0584ae24bc1732de4a3fc194b68c3be88db0/coverage-7.15.3-cp314-cp314-win_amd64.whl", hash = "sha256:64d0845f9c3ed47302bed265c15ab4dbb64aa4ec1490839b8e328f4e7fa914d2", size = 225246, upload-time = "2026-08-02T18:49:38.366Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/813a054371f3b018cc63c6bdb46a3c35d5e95d4e3ed4f1449d4196106db5/coverage-7.15.3-cp314-cp314-win_arm64.whl", hash = "sha256:69bc14684f8fbbee9f9dbaa4fe79719b0da9725fc37956785c06ec365acf6926", size = 224673, upload-time = "2026-08-02T18:49:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/8f/63/8c9f36cc71178d26db930baa03a4494abcc516d8d41bf820d0d85ef1d80b/coverage-7.15.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f92df943c24b96cb215ca26b4f6a2283e63c5db80f1635aceea7fff11311917b", size = 223298, upload-time = "2026-08-02T18:49:42.634Z" }, + { url = "https://files.pythonhosted.org/packages/54/66/211f24d058ce9f56ebf1420d55b7574fdae924f6da3836f83c8bd4793e38/coverage-7.15.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:66591c46bdd2971d3ae2bc503a5f0459c2edcaf6b7e045b292000cc95bc6cb95", size = 223568, upload-time = "2026-08-02T18:49:44.706Z" }, + { url = "https://files.pythonhosted.org/packages/dd/bb/9c2ad5574a0d6420a96c6cade4f8a683931b9e79fe609f8924d7b6964616/coverage-7.15.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa64458b81b18bfc67cdf1f6dc02b23e3edc672f2f8e11771fad75865415a43", size = 264932, upload-time = "2026-08-02T18:49:47.153Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/938c39e77bdd5a0a440412f975609ce3702dabbda6ac715719d93ca45a7b/coverage-7.15.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:447f5421ccf5475956cf516d4ca1d575f487947b6f4e11f9d80c6aefe24b3dc8", size = 267052, upload-time = "2026-08-02T18:49:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a3/7b431a98af35d9cc6394e54cde9435b33b8591672fbece6a4931267d7a8e/coverage-7.15.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0c77ef8cd483a4987a5d12d1d9d5f7ee598dfdc6c0844417d847e5768dc779", size = 269473, upload-time = "2026-08-02T18:49:51.599Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/dbc9951dce46be47a732823a1c571f62bcabdd54a68d8c281489a1a55cfb/coverage-7.15.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b273f4ff657446a06c2d85bf80e134fa869a92852ba5f87854a70e1fb44da77", size = 270591, upload-time = "2026-08-02T18:49:53.865Z" }, + { url = "https://files.pythonhosted.org/packages/71/bd/1d610772c7c0889bfe477a59c46ee66ea53e271f3f06951e9d55b317f7c6/coverage-7.15.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daea8c4fafa22488600405be2c2be525a9406fba3fc0a83acc726db3e14e2005", size = 264007, upload-time = "2026-08-02T18:49:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/69/97/852eb3dcdba156b1a9078503f098499916bf889f964b61ad4a08223ac169/coverage-7.15.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93ff57c530f3fa7aa69f92fb9b8892b8aa82712aa970842f4abf28657f42fb57", size = 266926, upload-time = "2026-08-02T18:49:57.944Z" }, + { url = "https://files.pythonhosted.org/packages/52/f8/b72cd238757fba2b587fc7dee047efe6e10b0c18343509faaaf502dd4680/coverage-7.15.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4df21bef8b800eebda9018f53d49c9ace3aeb0090c850139b27923aafcb83e91", size = 264529, upload-time = "2026-08-02T18:50:00.035Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0a/6c52ec4b7fb007cb6433d1fcfda4080cb15d75ad37ef9c31025f3427293e/coverage-7.15.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:db567b02685f26034adcbd85055f80d12cdf02111b8ed00886093d98b2874ce2", size = 268263, upload-time = "2026-08-02T18:50:02.161Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4e/f1f9aa3efd109a04353563a43fb5155340c1fdcdeaa6296ebed3b6f510ea/coverage-7.15.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5318dd51b8600b947e058cf5a4fe54d183d9d13c49b97b64ca7be05a34df9bef", size = 263377, upload-time = "2026-08-02T18:50:04.243Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fb/6b268a0b2728ef1c379ad656b899274477a5f6bed1bf6765b4b387fb0601/coverage-7.15.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c995bfa383c54704839b6c4c2627a1c00895597ada0e5e8190c81d8bd620555c", size = 265688, upload-time = "2026-08-02T18:50:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/29/54/1a3ea96e5d5e7cd41dc432597bfc60692910e635d05e1cc25a8ccc243581/coverage-7.15.3-cp314-cp314t-win32.whl", hash = "sha256:6433fafb8da0e1d02eb53411e0ecdadb6b88f0224fdc23317e703c0e88937d42", size = 225066, upload-time = "2026-08-02T18:50:08.533Z" }, + { url = "https://files.pythonhosted.org/packages/31/9d/a7b0d9afd18ed5274dd00651a78e7810a931c70d94b79996f150bec1a30f/coverage-7.15.3-cp314-cp314t-win_amd64.whl", hash = "sha256:fe578952b1b29fe8c777f43f241d49efac4b56724a3434f5d22ebe3c208df429", size = 225897, upload-time = "2026-08-02T18:50:10.572Z" }, + { url = "https://files.pythonhosted.org/packages/ca/11/34c5ae40b945e69aa72b87dc268135b7049905f3824af573b7073acbb946/coverage-7.15.3-cp314-cp314t-win_arm64.whl", hash = "sha256:d2e1acb7aee29dfa8f3e48c23f36670898baca1209d9bdd3985a50c7f982165e", size = 225212, upload-time = "2026-08-02T18:50:12.63Z" }, + { url = "https://files.pythonhosted.org/packages/37/e7/7069b3d6c018917f49ba2e1c5fb910e498c7fefa3a1b78cb1b79e61ff45d/coverage-7.15.3-py3-none-any.whl", hash = "sha256:da78fa6fc7dafe4212839173133ee85afcf42c5cd5f3e47fa7c1c210453b445e", size = 214297, upload-time = "2026-08-02T18:50:14.709Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "dill" version = "0.4.0" @@ -612,6 +715,20 @@ 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 = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "tomli" version = "2.3.0" @@ -693,7 +810,7 @@ wheels = [ [[package]] name = "youdotcom" -version = "2.6.0" +version = "3.0.0" source = { editable = "." } dependencies = [ { name = "httpcore" }, @@ -708,6 +825,7 @@ dev = [ { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov" }, ] [package.metadata] @@ -719,9 +837,10 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "mypy", specifier = ">=2.3.0" }, - { name = "pylint", specifier = ">=4.0.0" }, - { name = "pyright", specifier = ">=1.1.398" }, - { name = "pytest", specifier = ">=9.0.0" }, - { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "mypy", specifier = ">=2.3.0,<3" }, + { name = "pylint", specifier = ">=4.0.0,<5" }, + { name = "pyright", specifier = ">=1.1.398,<2" }, + { name = "pytest", specifier = ">=9.0.0,<10" }, + { name = "pytest-asyncio", specifier = ">=1.0.0,<2" }, + { name = "pytest-cov", specifier = ">=6.0.0,<8" }, ]