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 ff95852..0000000 --- a/.agents/skills/generate-sdk-and-open-pr/SKILL.md +++ /dev/null @@ -1,555 +0,0 @@ ---- -name: generate-sdk-and-open-pr -description: Generate the Speakeasy SDK for a new version and open a release PR -allowed-tools: Bash, Read, Write, Edit, Glob, Grep -metadata: - author: youdotcom-oss - version: "1.0.0" - category: release - keywords: release, version, publish, pypi ---- - -# Release - -Release a new version of the `youdotcom` Python SDK to PyPI and GitHub. - -## Step 1: Verify OpenAPI specs - -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 - -```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` - -If any versions are out of sync, warn the user before proceeding. - -## 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 -- **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 - -### 4a. Confirm SDK generation - -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 - -```bash -git checkout -b release/X.Y.Z -``` - -### 4f. Update version in all locations - -Update the version string in these files (if not already updated by Speakeasy): -- `pyproject.toml` — `version = "X.Y.Z"` -- `src/youdotcom/_version.py` — `__version__: str = "X.Y.Z"` and the `__user_agent__` string - -### 4g. Update markdown documentation - -#### CHANGELOG.md -Add a new section at the top (below the header), following the existing Keep a Changelog format: - -```markdown -## [X.Y.Z] - YYYY-MM-DD - -### Added -- ... - -### Changed -- ... - -### Removed -- ... -``` - -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. - -#### 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. - -### 4h. Update and validate tests - -After generation and doc updates, ensure the test suite is compatible with the new SDK code. - -#### Test structure - -- **Unit tests** (`tests/test_runs.py`, `tests/test_search.py`, `tests/test_contents.py`): Run against a Go mockserver in `tests/mockserver/`. These are auto-generated by Speakeasy. The mockserver is started via Docker or the compiled binary. -- **Integration tests** (`tests/test_live.py`): Run against the real You.com API. Require `YDC_API_KEY` env var. -- **Client tests** (`tests/test_client.py`): Test HTTP client setup helpers. - -#### 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 - -```bash -pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v -``` - -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) - -```bash -pytest tests/test_live.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: - -```bash -# Tests, docs, README, USAGE — everywhere except security.py (which has the fallback) -sed -i '' 's/YOU_API_KEY_AUTH/YDC_API_KEY/g' \ - README.md USAGE.md tests/*.py tests/README.md \ - docs/sdks/*/README.md .agents/skills/generate-sdk-and-open-pr/SKILL.md -``` - -Verify only `security.py` retains `YOU_API_KEY_AUTH` (the fallback): - -```bash -grep -rl "YOU_API_KEY_AUTH" --include="*.py" --include="*.md" . | grep -v __pycache__ | grep -v build/ | grep -v examples/ | 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" -) -``` - -If `YDC_API_KEY` is set in the environment, live tests will run against the real API. To run only unit tests (mockserver-based), exclude live tests: - -```bash -pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v -``` - -#### 4i-7. Run full validation suite - -After all post-generation fixes are applied, run the complete validation: - -```bash -# 1. Start mockserver -cd tests/mockserver && go run . & sleep 3 - -# 2. Unit tests (exclude live + performance) -cd ../.. && .venv/bin/python -m pytest tests/ --ignore=tests/test_live.py --ignore=tests/test_performance.py -v -# Expected: all pass - -# 3. Pylint -.venv/bin/pylint src/youdotcom/ --rcfile=pylintrc -# Expected: 10.00/10 - -# 4. Stop mockserver -kill $(lsof -ti:18080) -``` - -If any check fails, fix and re-run until all pass before committing. - -#### 4i-8. Verify auto-generated Search examples are valid - -Speakeasy assembles per-parameter `example` values into one combined request. For the Search API, three parameters have pairwise mutual-exclusion that the assembled example does not know about: - -- `include_domains` **cannot** be combined with `exclude_domains` (returns `422`). -- `boost_domains` **cannot** be combined with `include_domains` (returns `422`). -- `exclude_domains` + `boost_domains` **is** valid. - -After every regen, grep the lead Search examples in `USAGE.md` and `README.md` (specifically the `` blocks) to confirm no `search_post`/`search.unified` example combines all three of `include_domains`, `exclude_domains`, and `boost_domains`: - -```bash -# Each occurrence with all three is a bug — drop include_domains (keep -# exclude_domains + boost_domains, which is the only valid pair). -grep -nE 'include_domains=\[' USAGE.md README.md -# Expected: no matches. If any are listed, hand-fix by deleting the -# "include_domains=[...]" block (and the comma before it) from each. -``` - -The long-term fix lives upstream: add a request-level `example` block on `SearchRequestBody` / `SearchRequest` in `overlays/python_overlay.yaml` (or the front-end OpenAPI specs) that uses a single valid pair, so Speakeasy prefers that example instead of concatenating per-field ones. Track that as a follow-up; the hand-fix above is what keeps 2.4.0 correct in the meantime. - -Also scan for the `RetryConfig(...)` positional-after-kwargs regression that Speakeasy can produce when `search_post` is the lead example operation: - -```bash -# If you see ", RetryConfig(...)" or similar after a keyword argument in a -# search_post example, the generated Python is a SyntaxError. Fix by -# passing `retries=RetryConfig(...)`. -grep -nE ', RetryConfig\(' README.md USAGE.md -``` - -If anything matches, fix by hand (overlay-up fix is the same follow-up above). - -#### 4i-9. Audit empty-type / open-ended model schemas (`extra="ignore"` data-loss risk) - -When the OpenAPI spec defines a schema with no `properties` (e.g. an empty typed envelope used as `output.content` for `output_schema` requests, or `task.result` for completed background research), Speakeasy emits a `BaseModel` subclass whose body is the literal `pass`: - -```python -class Content(BaseModel): - pass -``` - -Pydantic's default config is `extra="ignore"`, so unknown JSON keys returned by the server are silently dropped at unmarshal. The SDK cannot recover them: `res.output.content.model_dump()` returns `{}`, not the structured dict. Users of `output_schema=` and background-mode research loathe this and have hit it in 2.4.0. - -**Detection** (after `speakeasy run`): - -```bash -# Every BaseModel whose body is just `pass` — these are the silent-drop -# candidates. Read each one alongside the field it backs and decide whether -# the user can recover the data via a different path. If not, fix it. -python3 - <<'PY' -import re, pathlib -for p in pathlib.Path("src/youdotcom/models").glob("*.py"): - for m in re.finditer( - r"^class (\w+)\(BaseModel\):\n((?:[ \t]+.*?\n)+)", - p.read_text(), re.MULTILINE, - ): - if m.group(2).strip() == "pass": - print(f"{p.name}: {m.group(1)}") -PY -``` - -**Spec-side fix (regen-durable).** Add `additionalProperties: true` to the schema in the OpenAPI specification. Speakeasy then generates `extra="allow"` on the resulting model and unknown keys round-trip intact — see the [Speakeasy additionalProperties docs](https://www.speakeasy.com/docs/sdks/customize/data-model/additionalproperties). - -Two ways to land it: - -- **Upstream spec**: Edit the responsible `*.yaml` in `~/Workspace/youdotcom-frontend/public/specs/` and let the next regen pick it up. -- **OpenAPI overlay** (`overlays/python_overlay.yaml`): inject the keyword without touching upstream — survives regens and lives with this SDK. Use the [RFC 9535 JSONPath syntax](https://github.com/speakeasy-api/openapi-overlay) (`x-speakeasy-jsonpath: rfc9535`) to match the existing overlay in this repo: - - ```yaml - overlay: 1.0.0 - x-speakeasy-jsonpath: rfc9535 - info: - title: Allow extras on open-ended response schemas (output_schema content + background task result) - version: 0.1.0 - actions: - # ResearchResponse.output.content has shape oneOf: [string, object] - # where the object branch is anonymous (no `properties`). Speakeasy - # currently emits `class Content(BaseModel): pass` (extra="ignore") and - # silently drops the structured payload returned by the server. - - target: $["components"]["schemas"]["ResearchResponse"]["properties"]["output"]["properties"]["content"]["oneOf"][1] - update: - additionalProperties: true - # TaskDetail.result is an anonymous object schema; same drop behaviour. - - target: $["components"]["schemas"]["TaskDetail"]["properties"]["result"] - update: - additionalProperties: true - ``` - - After applying the overlay and regen, verify the generated model now allows extras: - - ```bash - grep -nE "extra=\"allow\"|class Content\(BaseModel\):" \ - src/youdotcom/models/researchresponse.py - # Expect: from typing import ... ConfigDict ... model_config = ConfigDict(extra="allow") - # (or an equivalent annotation on the Content class) - - # If Content still has `pass` body and no extra="allow" config, the - # overlay didn't apply. Double-check the JSONPath against - # `.speakeasy/out.openapi.yaml`. - ``` - -**Workaround until the fix lands.** When the typed model drops data: - -- `research_helpers.py` docstring + CHANGELOG entry for `research_and_wait` MUST explicitly recommend the synchronous fallback (`client.research(..., background=False)` with the same `input`) and call out that `model_dump()` returns `{}`. -- `MIGRATION.md` `output_schema` example MUST show the same workaround rather than the misleading `output.content["..."]` syntax. -- `tests/test_research.py::TestResearchOutputSchema` MUST lock in `content_type.value == "object"` and the documented model_dump/emtpy-payload behaviour so a careless regen that re-introduces data loss fails loudly. -- Once the spec/overlay fix lands and regen produces `extra="allow"` models, simplify the workaround comments + drop the empty-payload lock-in assertion (replace with one that asserts the round-tripped dict). - -**Long-term.** Treat *empty* typed schemas as a red flag in spec review. Any schema backing a user-facing response field should declare `additionalProperties: true` (or a real schema) — never `{}` / no `properties`. Add a check to the front-end repo's CI (e.g. `scripts/audit-empty-schemas.ts`) so an empty schema in `youdotcom-frontend/public/specs/*.yaml` fails the build with a message pointing to this skill step. - -### 4j. Commit all changes - -Stage and commit all generated and manually updated files to the release branch: - -```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 push -u origin release/X.Y.Z -``` - -Open a PR against `main` using `gh`: - -```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 -)" -``` - -Report the PR URL to the user when done. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fba7158 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + # 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: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml new file mode 100644 index 0000000..2194fef --- /dev/null +++ b/.github/workflows/drift-check.yml @@ -0,0 +1,118 @@ +name: drift-check + +on: + schedule: + # Every Monday at 9:00 UTC + - cron: "0 9 * * 1" + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: drift-check + cancel-in-progress: false + +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 . + + # 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: | + set +e + OUTPUT=$(python scripts/check_drift.py --strict --verbose 2>&1) + EXIT_CODE=$? + echo "$OUTPUT" + # Random delimiter: a fixed "EOF" would break if it appeared in the output. + DELIM="drift_$(openssl rand -hex 8)" + { + echo "DRIFT_OUTPUT<<${DELIM}" + echo "$OUTPUT" + echo "${DELIM}" + } >> "$GITHUB_ENV" + echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" + exit 0 + + - 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: | + set -euo pipefail + gh label create drift --color FBCA04 \ + --description "SDK drift from OpenAPI specs" 2>/dev/null || true + + # 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 + + # 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 "::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: | + # 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 1c0689c..d480dde 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,12 +6,16 @@ on: push: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: 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 @@ -27,17 +31,79 @@ 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 ".[dev]" - pip install pytest pytest-asyncio + pip install --group dev -e . - name: Build and start mock server working-directory: tests/mockserver run: | go build -o mockserver . ./mockserver & - sleep 2 - curl -sf http://localhost:18080/ || echo "mock server ready" + 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 + + - name: Run mypy + run: mypy src/youdotcom/ + + # 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 + + 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: + - 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 tests - run: pytest tests/ -v --tb=short -x + - name: Run drift check (non-blocking) + run: python scripts/check_drift.py --verbose + continue-on-error: true diff --git a/.gitignore b/.gitignore index 9db7a30..8729423 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,13 @@ venv/ src/*.egg-info/ **/__pycache__/ .pytest_cache/ +.coverage +.coverage.* +htmlcov/ +.mypy_cache/ .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 75363e7..36ddeba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,66 @@ All notable changes to the You.com Python SDK will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.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, so 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 + +- **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=...)` | + +### 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. +- **`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, on both the sync and async paths. + + Debug logging is off by default (`get_default_logger()` returns a `NoOpLogger` unless `YOU_DEBUG` is set), so a default configuration was never affected. Callers who enabled debug output, via `YOU_DEBUG` or by passing their own `debug_logger`, previously had the API key written in plaintext to that logger's sink. If that applies to you and those logs left the host, rotate the key. + +### Changed + +- **An empty API key raises instead of falling back to the environment**: `You(api_key_auth="")`, a blank string, or a callable returning an empty string 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. +- **`__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. +- **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. +- **Added a `LICENSE` file**: the MIT license the README has always declared is now committed to the repository and bundled into the sdist and wheel. + +### 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 ### Added @@ -26,8 +86,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. @@ -35,10 +93,10 @@ print(detail.result.model_dump()["output"]["content"]) - **`you.stream_research_task(task_id=..., from_id=0)`**: Stream real-time updates for a background research task via `GET /v1/research/{task_id}/stream` (Server-Sent Events). Returns an `EventStream` of `ResearchTaskStreamEvent` objects. The connection closes automatically when the task reaches a terminal state. Terminal event names: `response.done`, `complete`, `completed` (success); `error`, `failed`, `cancelled` (failure). - **Convenience helpers** in `youdotcom.research_helpers` (hand-maintained, regen-safe): - - `research_background(you, ...)` / `research_background_async(you, ...)` — submit and return `TaskResponse` directly (no Union narrowing needed). - - `poll_research_task(you, task_id, ...)` / `poll_research_task_async(...)` — poll until terminal status (`completed`, `failed`, `cancelled`). Defaults: `interval_s=2.0`, `timeout_s=600.0` (10 minutes). For `frontier` tasks, pass `timeout_s=14400` (4 hours) explicitly since `poll_research_task` receives a `task_id` and cannot auto-detect the effort tier. - - `research_and_wait(you, ...)` / `research_and_wait_async(...)` — submit with `background=True`, then stream SSE events until a terminal event arrives, and fetch the final `TaskDetail`. If the stream times out or closes without a terminal event, a final `get_research_task` call resolves the status (returns the detail if completed, raises `RuntimeError` for terminal non-completed, or `TimeoutError` if still running). For polling instead of streaming, use `poll_research_task` directly. **`timeout_s` auto-adjusts** based on `research_effort` when omitted: 600s (10 min) for standard/deep/exhaustive, 14400s (4 hours) for `frontier`. - - `stream_research(you, task_id, ...)` / `stream_research_async(...)` — tolerant SSE iterator that surfaces undocumented event types as raw dicts instead of crashing on validation. **Recommended over `you.stream_research_task()` for real research tasks**, since the server emits intermediate workflow events not in the strict `Event` enum. + - `research_background(you, ...)` / `research_background_async(you, ...)`: submit and return `TaskResponse` directly (no Union narrowing needed). + - `poll_research_task(you, task_id, ...)` / `poll_research_task_async(...)`: poll until terminal status (`completed`, `failed`, `cancelled`). Defaults: `interval_s=2.0`, `timeout_s=600.0` (10 minutes). For `frontier` tasks, pass `timeout_s=14400` (4 hours) explicitly since `poll_research_task` receives a `task_id` and cannot auto-detect the effort tier. + - `research_and_wait(you, ...)` / `research_and_wait_async(...)`: submit with `background=True`, then stream SSE events until a terminal event arrives, and fetch the final `TaskDetail`. If the stream times out or closes without a terminal event, a final `get_research_task` call resolves the status (returns the detail if completed, raises `RuntimeError` for terminal non-completed, or `TimeoutError` if still running). For polling instead of streaming, use `poll_research_task` directly. **`timeout_s` auto-adjusts** based on `research_effort` when omitted: 600s (10 min) for standard/deep/exhaustive, 14400s (4 hours) for `frontier`. + - `stream_research(you, task_id, ...)` / `stream_research_async(...)`: tolerant SSE iterator that surfaces undocumented event types as raw dicts instead of crashing on validation. **Recommended over `you.stream_research_task()` for real research tasks**, since the server emits intermediate workflow events not in the strict `Event` enum. - **New models**: `TaskResponse`, `TaskResponseStatus`, `TaskDetail`, `TaskDetailStatus`, `TaskDetailInput`, `Result`, `GetResearchTaskRequest`, `StreamResearchTaskRequest`, `ResearchTaskStreamEvent`, `ResearchTaskStreamEventData`, `Event`, `ResearchResult` (Union alias). @@ -56,7 +114,7 @@ print(detail.result.model_dump()["output"]["content"]) ### Added -- **Finance Research API**: New `you.finance_research()` method on the main `You` client. The Finance Research API searches a finance-optimized index — SEC filings, earnings transcripts, analyst coverage, market data, and financial news — instead of the open web. Use it for earnings analysis, due diligence, and market research. +- **Finance Research API**: New `you.finance_research()` method on the main `You` client. The Finance Research API searches a finance-optimized index: SEC filings, earnings transcripts, analyst coverage, market data, and financial news, instead of the open web. Use it for earnings analysis, due diligence, and market research. ```python from youdotcom import You @@ -84,7 +142,7 @@ for source in res.output.sources: - **`Research.output.content` is now `Union[str, object]`**: When an `output_schema` is supplied, the server returns a structured JSON object and `content_type` becomes `"object"`. The overlay injects `additionalProperties: true` so `output.content` round-trips as a plain `dict` matching the requested schema. Text responses (`content_type="text"`) return `output.content` as a `str`. Check `output.content_type` to deserialise correctly: `text` → str, `object` → dict. -- **New `FinanceResearchEffort` enum**: The Finance Research API has its own effort enum (`DEEP`, `EXHAUSTIVE`) distinct from the Research API's `ResearchEffort`. Both have clean names — `ResearchEffort` is unchanged from 2.3.x. +- **New `FinanceResearchEffort` enum**: The Finance Research API has its own effort enum (`DEEP`, `EXHAUSTIVE`) distinct from the Research API's `ResearchEffort`. Both have clean names. `ResearchEffort` is unchanged from 2.3.x. - **Livecrawl formats parameter now requires a list**: The `livecrawl_formats` parameter is now strictly typed as `Optional[List[LiveCrawlFormats]]`. Passing a single enum value (which worked in prior versions) now raises a validation error. Wrap the value in a list: @@ -96,7 +154,7 @@ you.search.unified(query="...", livecrawl_formats=LiveCrawlFormats.MARKDOWN) you.search.unified(query="...", livecrawl_formats=[LiveCrawlFormats.MARKDOWN]) ``` -- **Consolidated error classes for Search**: The bare-from-spec names removed in 2.4.0 (`SearchForbiddenError`, `SearchUnauthorizedError`, `UnprocessableEntityError`, etc.) are replaced for both Search endpoints (`you.search.unified()` GET and `you.search_post()` POST) by consolidated `UnprocessableEntityResponseError`, `UnauthorizedResponseError`, and `ForbiddenResponseError`. `you.research()` and `you.finance_research()` keep raising per-endpoint typed errors (`ResearchUnprocessableEntityError`, `FinanceResearchUnprocessableEntityError`, etc.) — those classes are NOT consolidated. Catch Search on the consolidated `*ResponseError` class or `YouDefaultError`; catch Research/Finance Research on the per-endpoint class. +- **Consolidated error classes for Search**: The bare-from-spec names removed in 2.4.0 (`SearchForbiddenError`, `SearchUnauthorizedError`, `UnprocessableEntityError`, etc.) are replaced for both Search endpoints (`you.search.unified()` GET and `you.search_post()` POST) by consolidated `UnprocessableEntityResponseError`, `UnauthorizedResponseError`, and `ForbiddenResponseError`. `you.research()` and `you.finance_research()` keep raising per-endpoint typed errors (`ResearchUnprocessableEntityError`, `FinanceResearchUnprocessableEntityError`, etc.). Those classes are NOT consolidated. Catch Search on the consolidated `*ResponseError` class or `YouDefaultError`; catch Research/Finance Research on the per-endpoint class. - **`WebResult.authors` field removed**: The `authors` field has been removed from the web search result model (`WebResult` / `WebResultTypedDict`). The server no longer returns this field. The overlay includes a `remove` action so future regenerations stay aligned. @@ -106,31 +164,20 @@ you.search.unified(query="...", livecrawl_formats=[LiveCrawlFormats.MARKDOWN]) # Before (2.3.x) export YOU_API_KEY_AUTH="your-api-key" -# After (2.4.0) — preferred +# After (2.4.0), preferred export YDC_API_KEY="your-api-key" # YOU_API_KEY_AUTH still works as a fallback ``` ### Notes -- The `unresearched` `ulow` effort level remains internal and is intentionally NOT exposed in the SDK — it is consolidated as internal routing on the server. +- The `unresearched` `ulow` effort level remains internal and is intentionally NOT exposed in the SDK. It is consolidated as internal routing on the server. - `you.finance_research()` deliberately does not support `source_control` or `output_schema`. The Finance Research API runs against a finance-optimized index and returns Markdown-formatted answers only. - **`pydantic` upper bound removed**: The SDK previously pinned `pydantic <2.13` as a defensive measure. For a published library, upper bounds on core deps create resolver conflicts for downstream consumers who need a newer pydantic for other packages (fastapi, langchain, etc.). The SDK uses only stable pydantic 2.x APIs (`model_dump`, `model_serializer`, `BaseModel`, `pydantic_core.core_schema`), and the overlay's `additionalProperties: true` → `Dict[str, Any]` mechanism is plain Python typing, not a pydantic feature. The lower bound `>=2.11.2` is retained; if a future pydantic release breaks something, CI will catch it and we'll pin reactively. ### 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. --- @@ -161,7 +208,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 @@ -317,7 +363,7 @@ from youdotcom.models import ( | Old Name (1.x) | New Name (2.0) | Reason | |----------------|----------------|--------| -| `Verbosity` | `ReportVerbosity` | More specific—clarifies it controls research report verbosity | +| `Verbosity` | `ReportVerbosity` | More specific, clarifies it controls research report verbosity | | `Format` | `ContentsFormats` | Avoids collision with Python's built-in `format()`, plural indicates array usage | | `AgentType` | *Removed* | Replaced by typed request classes | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d585717..fa339fb 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 --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. + +## 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/LICENSE b/LICENSE new file mode 100644 index 0000000..4512edd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 You.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MIGRATION.md b/MIGRATION.md index 1a52ebd..b6911b8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,196 @@ # Migration Guide -## 2.4.0 → 2.5.0 (Latest) +## 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 an empty string | 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`: + +```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 Moved to Direct Method + +`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 (3.0.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. This behavior was already the case in 2.5.0; it is documented here for reference. + +### 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 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: + +| 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 +234,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). @@ -73,7 +246,7 @@ from youdotcom.models import ResearchEffort, ResearchResponse you = You() res = you.research(input="...", research_effort=ResearchEffort.DEEP) -# res is ResearchResponse — same as 2.4.0 +# res is ResearchResponse, same as 2.4.0 assert isinstance(res, ResearchResponse) ``` @@ -202,7 +375,7 @@ from youdotcom.models import FinanceResearchEffort you.finance_research(input="...", research_effort=FinanceResearchEffort.DEEP) ``` -`ResearchEffort` keeps the name `ResearchEffort` and values `LITE`, `STANDARD`, `DEEP`, `EXHAUSTIVE`. No migration is required — the OpenAPI spec was promoted to a named schema so the SDK preserves the clean name. +`ResearchEffort` keeps the name `ResearchEffort` and values `LITE`, `STANDARD`, `DEEP`, `EXHAUSTIVE`. No migration is required. The OpenAPI spec was promoted to a named schema so the SDK preserves the clean name. #### `livecrawl_formats` now requires a list @@ -252,9 +425,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 @@ -270,7 +442,7 @@ The SDK now reads `YDC_API_KEY` (canonical per `you.com/docs`) instead of `YOU_A # Before (2.3.x) export YOU_API_KEY_AUTH="your-api-key" -# After (2.4.0) — preferred +# After (2.4.0), preferred export YDC_API_KEY="your-api-key" # YOU_API_KEY_AUTH still works as a fallback ``` @@ -293,7 +465,7 @@ from youdotcom.errors import UnprocessableEntityError from youdotcom.errors import ( ResearchUnprocessableEntityError, # research-specific FinanceResearchUnprocessableEntityError, # new - YouError, # safety net — catches every SDK-raised error + YouError, # safety net, catches every SDK-raised error ) try: @@ -310,12 +482,12 @@ The bare `UnprocessableEntityError` / `SearchUnauthorizedError` / `SearchForbidd ### New APIs to Try -- `you.finance_research(input=..., research_effort=FinanceResearchEffort.DEEP)` — finance-optimized index. +- `you.finance_research(input=..., research_effort=FinanceResearchEffort.DEEP)`: finance-optimized index. -- `you.research(..., source_control={...})` — restrict / boost / exclude domains or filter by recency or country. -- `you.research(..., output_schema={...})` — structured JSON output. -- `you.search_post(..., boost_domains=[...])` (POST takes a list) or `you.search.unified(..., boost_domains="nytimes.com,wired.com")` (GET takes a single comma-separated string) — boost (but don't restrict) matching domains in ranking. -- `you.contents.generate(..., max_age=86400)` — require cached content younger than 24 hours. +- `you.research(..., source_control={...})`: restrict / boost / exclude domains or filter by recency or country. +- `you.research(..., output_schema={...})`: structured JSON output. +- `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 +534,8 @@ res = you.contents.generate(urls=["https://example.com"], crawl_timeout=5) ## 1.x to 2.0 +> **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. ## Quick Reference diff --git a/README.md b/README.md index d9d0461..00415a0 100644 --- a/README.md +++ b/README.md @@ -1,711 +1,431 @@
- image + You.com
+
-The official developer-friendly & type-safe Python SDK specifically designed to leverage the You.com API. + The official Python SDK for the You.com API: web search, citation-backed answers, page contents, and multi-step research.
-
+
- - - - + PyPI + Python versions + Documentation + License: MIT
- -## 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 -- **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 - - - -## Table of Contents - - * [SDK Installation](#sdk-installation) - * [IDE Support](#ide-support) - * [SDK Example Usage](#sdk-example-usage) - * [Authentication](#authentication) - * [Available Resources and Operations](#available-resources-and-operations) - * [Server-sent event streaming](#server-sent-event-streaming) - * [Retries](#retries) - * [Error Handling](#error-handling) - * [Server Selection](#server-selection) - * [Custom HTTP Client](#custom-http-client) - * [Resource Management](#resource-management) - * [Debugging](#debugging) -* [Development](#development) - * [Maturity](#maturity) - * [Testing](#testing) - * [Contributions](#contributions) - - - - -## SDK Installation - -> [!NOTE] -> **Python version upgrade policy** -> -> Once a Python version reaches its [official end of life date](https://devguide.python.org/versions/), a 3-month grace period is provided for users to upgrade. Following this grace period, the minimum python version supported in the SDK will be updated. - -The SDK can be installed with *uv*, *pip*, or *poetry* package managers. - -### uv - -*uv* is a fast Python package installer and resolver, designed as a drop-in replacement for pip and pip-tools. It's recommended for its speed and modern Python tooling capabilities. +## Install ```bash -uv add youdotcom +pip install youdotcom ``` -### PIP +Requires Python 3.10+. Also available via `uv add youdotcom` or `poetry add youdotcom`. -*PIP* is the default package installer for Python, enabling easy installation and management of packages from PyPI via the command line. +## Quickstart -```bash -pip install youdotcom -``` +Get an API key from [you.com/platform](https://you.com/platform) and set it as `YDC_API_KEY`. + +```python +import os +from youdotcom import You -### Poetry +with You(api_key_auth=os.getenv("YDC_API_KEY"), timeout_ms=60_000) as you: + res = you.answer(query="What caused the 2008 financial crisis?") + print(res.answer) +``` -*Poetry* is a modern tool that simplifies dependency management and package publishing by using a single `pyproject.toml` file to handle project metadata and dependencies. +That prints a markdown answer with inline `[[1, 2]]` citations. The sources behind +them are on the response: -```bash -poetry add youdotcom +```python + for citation in res.citations or []: + print(citation.source, citation.excerpts) ``` -### Shell and script usage with `uv` +Two things about that snippet worth knowing up front: -You can use this SDK in a Python shell with [uv](https://docs.astral.sh/uv/) and the `uvx` command that comes with it like so: +> **`timeout_ms` is doing real work.** Without it, requests inherit httpx's 5 +> second default, and `answer` takes longer than that. See [Timeouts](#timeouts). +> +> **The key is explicit here, but it doesn't have to be.** Pass +> `api_key_auth=None`, or omit it, to read `YDC_API_KEY` from the environment. +> See [Authentication](#authentication) for the resolution order. -```shell -uvx --from youdotcom python -``` +## The APIs -It's also possible to write a standalone Python script without needing to set up a whole project like so: +Every method is a direct call on `You`, and every one has an `_async` twin with +the same signature. -```python -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "youdotcom", -# ] -# /// +`search()` and `answer()` normalize their enum-typed parameters, so plain +strings work in any case. `country="us"` and `safesearch="STRICT"` are both +accepted. Elsewhere, pass the value as the API spells it (all lowercase) or +import the enum from `youdotcom.models`. -from youdotcom import You +### Answer + +A synthesized answer with citations, grounded in live web results. -sdk = You( - # SDK arguments +```python +res = you.answer( + query="What are the tradeoffs of vector vs. keyword search?", + freshness="month", + include_domains=["arxiv.org"], ) -# Rest of script here... +res.answer # markdown, with inline [[n]] citations +res.citations # [AnswerCitation(source, excerpts)] +res.results.web # results used during synthesis ``` -Once that is saved to a file, you can run it with `uv run script.py` where -`script.py` can be replaced with the actual file name. - +### Search - -## IDE Support +Ranked web and news results. -### PyCharm +```python +res = you.search( + query="EU AI Act enforcement timeline", + count=10, + country="us", + freshness="week", +) -Generally, the SDK will work well with most IDEs out of the box. However, when using PyCharm, you can enjoy much better integration with Pydantic by installing an additional plugin. +for hit in res.results.web or []: + print(hit.title, hit.url) +``` -- [PyCharm Pydantic Plugin](https://docs.pydantic.dev/latest/integrations/pycharm/) - +`include_domains` restricts results to an allowlist; `exclude_domains` and +`boost_domains` filter and re-rank. `include_domains` cannot be combined with +either of the others. The API returns `422` if you try. Search also supports +[search operators](https://you.com/docs/guides/search-operators). - -## SDK Example Usage +### Contents -### Example +Clean HTML or Markdown for a list of URLs. ```python -# Synchronous Example -import os -from youdotcom import You, models +pages = you.contents( + urls=["https://example.com", "https://you.com"], + formats=["markdown", "metadata"], +) + +for page in pages: + print(page.url, page.title) + print(page.markdown) +``` +`formats` accepts `html`, `markdown`, and `metadata` (JSON-LD and OpenGraph). +Use `max_age` to reject cached content older than a given number of seconds. -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: +### Research - res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ - "spam-site.com", - "other-site.com", - ], boost_domains=[ - "nytimes.com", - "wired.com", - ], crawl_timeout=10) +Multi-step research with reasoning and cited sources. Higher effort levels run +more searches and take longer. + +```python +res = you.research( + input="Compare the unit economics of the major cloud providers", + research_effort="deep", # lite | standard | deep | exhaustive | frontier +) - # Handle response - print(res) +print(res.output.content) +for source in res.output.sources or []: + print(source.url) ``` -
+`you.finance_research()` is the finance-tuned counterpart, taking +`research_effort` of `deep` or `exhaustive`. -The same SDK client can also be used to make asynchronous requests by importing asyncio. +Deep and exhaustive runs can take minutes, and `frontier` runs far longer. For +anything beyond `standard`, use [background mode](#long-running-research). + +## Async + +Every method has an `_async` variant. Use `async with` so both transports are +released on exit. ```python -# Asynchronous Example import asyncio import os -from youdotcom import You, models +from youdotcom import You async def main(): - - async with You( - 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=[ - "spam-site.com", - "other-site.com", - ], boost_domains=[ - "nytimes.com", - "wired.com", - ], crawl_timeout=10) - - # Handle response - print(res) + async with You(api_key_auth=os.getenv("YDC_API_KEY")) as you: + res = await you.answer_async(query="What is retrieval-augmented generation?") + print(res.answer) asyncio.run(main()) ``` - -For more thorough examples of how to use our APIs, including typesafe patterns, see `api-example-calls.py` under the `examples` folder. - -## Authentication +Concurrent calls share the one client: + +```python +answer, results = await asyncio.gather( + you.answer_async(query="What is RAG?"), + you.search_async(query="RAG benchmarks", count=5), +) +``` -### Per-Client Security Schemes +## Long-running research -This SDK supports the following security scheme globally: +Rather than holding a request open for minutes, background mode submits the task +and returns immediately. The helpers in `youdotcom.research_helpers` cover the +common shapes. -| Name | Type | Scheme | Environment Variable | -| -------------- | ------ | ------- | -------------------- | -| `api_key_auth` | apiKey | API key | `YDC_API_KEY` | +**Submit and wait.** Handles submission, streaming, and the final fetch: -To authenticate with the API the `api_key_auth` parameter must be set when initializing the SDK client instance. For example: ```python -import os -from youdotcom import You, models +from youdotcom.research_helpers import research_and_wait +detail = research_and_wait( + you, + input="Survey the state of solid-state battery commercialization", + research_effort="exhaustive", +) +print(detail.status, detail.result) +``` -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: +The wait is bounded automatically: 10 minutes for standard, deep, and +exhaustive, 4 hours for `frontier`. Pass `timeout_s` to override. It raises +`TimeoutError` if no terminal event arrives, and `RuntimeError` if the task ends +in a non-completed state. - res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ - "spam-site.com", - "other-site.com", - ], boost_domains=[ - "nytimes.com", - "wired.com", - ], crawl_timeout=10) +**Submit and poll**, if you'd rather own the loop: - # Handle response - print(res) +```python +from youdotcom.research_helpers import research_background, poll_research_task +task = research_background(you, input="...", research_effort="deep") +detail = poll_research_task(you, task.task_id, interval_s=5.0) ``` - - -## Available Resources and Operations +**Stream events** as the task progresses: -
-Available methods - -### [You SDK](docs/sdks/you/README.md) +```python +from youdotcom.research_helpers import stream_research -* [search_post](docs/sdks/you/README.md#search_post) - Returns a list of unified search results from web and news sources -* [research](docs/sdks/you/README.md#research) - Returns comprehensive research-grade answers with multi-step reasoning -* [get_research_task](docs/sdks/you/README.md#get_research_task) - Get the status of a background research task -* [stream_research_task](docs/sdks/you/README.md#stream_research_task) - Stream updates for a background research task -* [finance_research](docs/sdks/you/README.md#finance_research) - Returns comprehensive finance-grade research answers with multi-step reasoning +for evt in stream_research(you, task_id=task.task_id): + print(evt.event, evt.data) + if evt.event in ("response.done", "completed", "error", "failed", "cancelled"): + break +``` -### [Agents.Runs](docs/sdks/runs/README.md) +`stream_research()` tolerates event names outside the documented set, yielding +them as raw dicts. Prefer it over `you.stream_research_task()`, which validates +strictly and will raise on an unrecognized event. Pass `from_id` to resume a +stream after a disconnect. -* [create](docs/sdks/runs/README.md#create) - Run an Agent +Each helper has an `_async` twin: `research_and_wait_async`, +`research_background_async`, `poll_research_task_async`, `stream_research_async`. -### [Contents](docs/sdks/contentssdk/README.md) +## Authentication -* [generate](docs/sdks/contentssdk/README.md#generate) - Returns the content of the web pages +The API key is sent as the `X-API-Key` header. How it's resolved: -### [Search](docs/sdks/search/README.md) +| `api_key_auth` | 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 an empty string | Raises `ValueError` | -* [unified](docs/sdks/search/README.md#unified) - Returns a list of unified search results from web and news sources +Every endpoint requires a key, so an empty string is never valid. It means a key +was expected and none arrived. The SDK raises rather than reading the +environment, since falling back would run the request under whatever identity +the environment happens to hold instead of the one the code asked for. -
- +In practice that shows up as `os.getenv("YDC_API_KEY", "")` with the variable +unset. Use `os.getenv("YDC_API_KEY")`. `None` is how you ask for the lookup. - -## Server-sent event streaming +A callable is resolved on each request, so it can return a rotating key. -[Server-sent events][mdn-sse] are used to stream content from certain -operations. These operations will expose the stream as [Generator][generator] that -can be consumed using a simple `for` loop. The loop will -terminate when the server no longer has any events to send and closes the -underlying connection. +## Errors -The stream is also a [Context Manager][context-manager] and can be used with the `with` statement and will close the -underlying connection when the context is exited. +Every API error subclasses `YouError`, which carries `.message`, +`.status_code`, `.body`, `.headers`, and `.raw_response`. The typed subclasses +below add a parsed `.data`. ```python -import os -from youdotcom import You -from youdotcom.models import ( - ExpressAgentRunsRequest, - WebSearchTool, - ResponseCreated, - ResponseStarting, - ResponseOutputItemAdded, - ResponseOutputContentFull, - ResponseOutputTextDelta, - ResponseOutputItemDone, - ResponseDone, +from youdotcom.errors import ( + PaymentRequiredResponseError, + UnauthorizedResponseError, + YouError, ) -from youdotcom.utils import eventstreaming - - -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() - ] - )) - - # 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 - 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__}") + +try: + res = you.answer(query="...") +except UnauthorizedResponseError: + ... # 401, bad or missing key +except PaymentRequiredResponseError as e: + print(e.data.message, e.data.upgrade_url) # 402, out of credits +except YouError as e: + print(e.status_code, e.body) # anything else from the API ``` -[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 - +Answer and search share one set of error classes; research, finance research, +contents, and the task endpoints each raise their own, so you can catch a `422` +from research without catching one from search. - -## Retries +| Status | Answer / Search | Contents | Research | Finance Research | Task get / stream | +| --- | --- | --- | --- | --- | --- | +| 401 | `UnauthorizedResponseError` | `ContentsUnauthorizedError` | `ResearchUnauthorizedError` | `FinanceResearchUnauthorizedError` | `GetResearchTask…` / `StreamResearchTask…UnauthorizedError` | +| 402 | `PaymentRequiredResponseError` answer only | n/a | n/a | n/a | n/a | +| 403 | `ForbiddenResponseError` | `ContentsForbiddenError` | `ResearchForbiddenError` | `FinanceResearchForbiddenError` | `…ForbiddenError` | +| 404 | n/a | n/a | n/a | n/a | `…NotFoundError` | +| 422 | `UnprocessableEntityResponseError` | n/a | `ResearchUnprocessableEntityError` | `FinanceResearchUnprocessableEntityError` | n/a | +| 500 | `InternalServerErrorResponse` | `ContentsInternalServerError` | `ResearchInternalServerError` | `FinanceResearchInternalServerError` | `…InternalServerError` | -Some of the endpoints in this SDK support retries. If you use the SDK without any configuration, it will fall back to the default retry strategy provided by the API. However, the default retry strategy can be overridden on a per-operation basis, or across the entire SDK. +Two errors sit outside that table: `ResponseValidationError` when a response +doesn't match its model, and `httpx.RequestError` (and subclasses) for transport +failures such as connection resets and timeouts. -To change the default retry strategy for a single API call, simply provide a `RetryConfig` object to the call: -```python -import os -from youdotcom import You, models -from youdotcom.utils import BackoffStrategy, RetryConfig +## Configuration +### Retries -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: +The SDK does **not** retry by default. Opt in per call or for the whole client: - res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ - "spam-site.com", - "other-site.com", - ], boost_domains=[ - "nytimes.com", - "wired.com", - ], crawl_timeout=10, - retries=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False)) - - # Handle response - print(res) - -``` - -If you'd like to override the default retry strategy for all operations that support retries, you can use the `retry_config` optional parameter when initializing the SDK: ```python -import os -from youdotcom import You, models from youdotcom.utils import BackoffStrategy, RetryConfig +retries = RetryConfig( + "backoff", + BackoffStrategy(initial_interval=500, max_interval=10_000, exponent=1.5, max_elapsed_time=60_000), + retry_connection_errors=True, +) -with You( - retry_config=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False), - 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=[ - "spam-site.com", - "other-site.com", - ], boost_domains=[ - "nytimes.com", - "wired.com", - ], crawl_timeout=10) - - # Handle response - print(res) - +with You(api_key_auth=key, retry_config=retries) as you: # whole client + res = you.search(query="...", retries=retries) # or one call ``` - - -## Error Handling +Retries apply to `429`, `500`, `502`, `503`, and `504`. -[`YouError`](./src/youdotcom/errors/youerror.py) is the base class for all HTTP error responses. It has the following properties: +### Timeouts -| Property | Type | Description | -| ------------------ | ---------------- | --------------------------------------------------------------------------------------- | -| `err.message` | `str` | Error message | -| `err.status_code` | `int` | HTTP response status code eg `404` | -| `err.headers` | `httpx.Headers` | HTTP response headers | -| `err.body` | `str` | HTTP body. Can be empty string if no body is returned. | -| `err.raw_response` | `httpx.Response` | Raw HTTP response | -| `err.data` | | Optional. Some errors may contain structured data. [See Error Classes](#error-classes). | +**Set one.** With no `timeout_ms`, requests inherit the underlying httpx client's +default of 5 seconds, which is far too short for `answer`, `research`, and +`finance_research`. Those endpoints routinely take tens of seconds, so a call +without a timeout will raise `httpx.ReadTimeout` before the API responds. -### Example -```python -import os -from youdotcom import You, errors, models - - -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as 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=[ - "spam-site.com", - "other-site.com", - ], boost_domains=[ - "nytimes.com", - "wired.com", - ], crawl_timeout=10) - - # Handle response - print(res) - - - except errors.YouError as e: - # The base class for HTTP error responses - print(e.message) - print(e.status_code) - print(e.body) - print(e.headers) - print(e.raw_response) - - # Depending on the method different errors may be thrown - if isinstance(e, errors.UnauthorizedResponseError): - print(e.data.detail) # Optional[str] -``` +`timeout_ms` applies to the whole client or to a single call: -### Error Classes -**Primary error:** -* [`YouError`](./src/youdotcom/errors/youerror.py): The base class for HTTP error responses. - -
Less common errors (31) - -
- -**Network errors:** -* [`httpx.RequestError`](https://www.python-httpx.org/exceptions/#httpx.RequestError): Base class for request errors. - * [`httpx.ConnectError`](https://www.python-httpx.org/exceptions/#httpx.ConnectError): HTTP client was unable to make a request to a server. - * [`httpx.TimeoutException`](https://www.python-httpx.org/exceptions/#httpx.TimeoutException): HTTP request timed out. - - -**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.* -* [`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. - -
- -\* Check [the method documentation](#available-resources-and-operations) to see if the error is applicable. - - - -## Server Selection - -### Override Server URL Per-Client - -The default server can be overridden globally by passing a URL to the `server_url: str` optional parameter when initializing the SDK client instance. For example: ```python -import os -from youdotcom import You, models - - -with You( - server_url="https://api.you.com", - 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) - - # Handle response - print(res) - +with You(api_key_auth=key, timeout_ms=60_000) as you: + answer = you.answer(query="...") # inherits 60s + results = you.search(query="...", timeout_ms=10_000) # this call only ``` -### Override Server URL Per-Operation +Search and contents are fast enough for the default. Research in background mode +is the exception: the helpers under +[Long-running research](#long-running-research) manage their own deadlines, so +`timeout_s` there bounds the wait rather than `timeout_ms`. -The server URL can also be overridden on a per-operation basis, provided a server list was specified for the operation. For example: -```python -import os -from youdotcom import You, models - - -with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), -) as you: - - res = you.search_post(query="What are the latest geopolitical updates from India", count=10, language=models.Language.EN, exclude_domains=[ - "spam-site.com", - "other-site.com", - ], boost_domains=[ - "nytimes.com", - "wired.com", - ], crawl_timeout=10, server_url="https://ydc-index.io") +### Servers - # Handle response - print(res) +`search` and `contents` go to `https://ydc-index.io`. Everything else goes to +`https://api.you.com`: `answer`, `research`, `finance_research`, and the research +task endpoints. The SDK routes each call for you. To point one call +elsewhere, at a proxy or a test server, pass `server_url` to the method: +```python +res = you.search(query="...", server_url="http://localhost:18080") ``` - - -## Custom HTTP Client +The constructor's `server_url` sets the default host, which affects the +`api.you.com` endpoints. Because `search` and `contents` have their own +per-operation default, they are unaffected by it; override those per call. + +### Custom HTTP client -The Python SDK makes API calls using the [httpx](https://www.python-httpx.org/) HTTP library. In order to provide a convenient way to configure timeouts, cookies, proxies, custom headers, and other low-level configuration, you can initialize the SDK client with your own HTTP client instance. -Depending on whether you are using the sync or async version of the SDK, you can pass an instance of `HttpClient` or `AsyncHttpClient` respectively, which are Protocol's ensuring that the client has the necessary methods to make API calls. -This allows you to wrap the client with your own custom logic, such as adding custom headers, logging, or error handling, or you can just pass an instance of `httpx.Client` or `httpx.AsyncClient` directly. +Pass any `httpx.Client` / `httpx.AsyncClient` to control proxies, TLS, custom +headers, or connection limits: -For example, you could specify a header for every request that this sdk makes as follows: ```python -from youdotcom import You import httpx -http_client = httpx.Client(headers={"x-custom-header": "someValue"}) -s = You(client=http_client) -``` +http_client = httpx.Client(proxy="http://localhost:8030", headers={"x-team": "search"}) -or you could wrap the client with your own custom logic: -```python -from youdotcom import You -from youdotcom.httpclient import AsyncHttpClient -import httpx +with You(api_key_auth=key, client=http_client) as you: + ... -class CustomClient(AsyncHttpClient): - client: AsyncHttpClient - - def __init__(self, client: AsyncHttpClient): - self.client = client - - async def send( - self, - request: httpx.Request, - *, - stream: bool = False, - auth: Union[ - httpx._types.AuthTypes, httpx._client.UseClientDefault, None - ] = httpx.USE_CLIENT_DEFAULT, - follow_redirects: Union[ - bool, httpx._client.UseClientDefault - ] = httpx.USE_CLIENT_DEFAULT, - ) -> httpx.Response: - request.headers["Client-Level-Header"] = "added by client" - - return await self.client.send( - request, stream=stream, auth=auth, follow_redirects=follow_redirects - ) - - def build_request( - self, - method: str, - url: httpx._types.URLTypes, - *, - content: Optional[httpx._types.RequestContent] = None, - data: Optional[httpx._types.RequestData] = None, - files: Optional[httpx._types.RequestFiles] = None, - json: Optional[Any] = None, - params: Optional[httpx._types.QueryParamTypes] = None, - headers: Optional[httpx._types.HeaderTypes] = None, - cookies: Optional[httpx._types.CookieTypes] = None, - timeout: Union[ - httpx._types.TimeoutTypes, httpx._client.UseClientDefault - ] = httpx.USE_CLIENT_DEFAULT, - extensions: Optional[httpx._types.RequestExtensions] = None, - ) -> httpx.Request: - return self.client.build_request( - method, - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - timeout=timeout, - extensions=extensions, - ) - -s = You(async_client=CustomClient(httpx.AsyncClient())) +http_client.close() # a transport you supply is yours to close ``` - - -## Resource Management +You can also pass anything satisfying the `HttpClient` / `AsyncHttpClient` +protocols in `youdotcom.httpclient` to wrap requests with your own logic. Note +that a transport you supply is yours to close. The SDK only closes the ones it +creates. -The `You` class implements the context manager protocol and registers a finalizer function to close the underlying sync and async HTTPX clients it uses under the hood. This will close HTTP connections, release memory and free up other resources held by the SDK. In short-lived Python programs and notebooks that make a few SDK method calls, resource management may not be a concern. However, in longer-lived programs, it is beneficial to create a single SDK instance via a [context manager][context-manager] and reuse it across the application. +### Resource management -[context-manager]: https://docs.python.org/3/reference/datamodel.html#context-managers +`You` holds open connections and has no public `close()`, so use it as a context +manager. Both transports are released on exit. ```python -import os -from youdotcom import You -def main(): - - with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), - ) as you: - # Rest of application here... - - -# Or when using async: -async def amain(): - - async with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), - ) as you: - # Rest of application here... +with You(api_key_auth=key) as you: + ... +# or: async with You(api_key_auth=key) as you: ``` - - -## Debugging +An instance is not reusable after the block exits, including for calls of the +other flavor. Use one instance per sync/async flavor. + +### Debug logging -You can setup your SDK to emit debug logs for SDK requests and responses. +Set `YOU_DEBUG=1` for request and response logging, or pass your own logger: -You can pass your own logger class directly into your SDK. ```python -from youdotcom import You import logging -logging.basicConfig(level=logging.DEBUG) -s = You(debug_logger=logging.getLogger("youdotcom")) +with You(api_key_auth=key, debug_logger=logging.getLogger("youdotcom")) as you: + ... ``` -You can also enable a default debug logger by setting an environment variable `YOU_DEBUG` to true. - +`Authorization`, `X-API-Key`, `Cookie`, and `Set-Cookie` are redacted. Request +and response bodies are not, and may carry sensitive data. Don't enable debug +logging in production, and don't commit debug logs to version control. - +## Documentation -# Development +- [API reference](https://you.com/docs/api-reference): endpoints, parameters, response schemas +- [Quickstart](https://you.com/docs/quickstart) +- [Pricing and plans](https://you.com/platform) +- [`docs/`](docs/): per-method SDK reference generated from this codebase +- [`examples/`](examples/): runnable, typed examples for every endpoint +- [MIGRATION.md](MIGRATION.md): upgrading between major versions -## Maturity +## Development -This SDK is in beta, and there may be breaking changes between versions without a major version update. Therefore, we recommend pinning usage -to a specific package version. This way, you can install the same version each time without breaking changes unless you are intentionally -looking for the latest version. - -## Testing - -The SDK includes a comprehensive test suite that covers all API endpoints with success and error scenarios. Tests are written using pytest and run against a mock server. - -To run the test suite: +### Testing ```bash ./scripts/run_tests.sh ``` -This script automatically: -- Starts the mock server (requires Go or Docker) -- Sets up a Python virtual environment -- Installs dependencies -- Runs all tests -- Cleans up the mock server +Starts the Go mock server, sets up a virtualenv, runs the suite, and cleans up. +Pass `--cleanup` to remove the virtualenv afterwards. See +[tests/README.md](tests/README.md) for running pieces of it directly. -By default, the virtual environment is kept for faster subsequent test runs. To remove it after tests complete: +### Drift detection + +This SDK is hand-maintained rather than generated, so `scripts/check_drift.py` +enforces what code generation used to guarantee: it diffs the published OpenAPI +specs against the SDK surface (endpoints, server URLs, enum values, request +parameters, response fields) and runs on every PR plus weekly. ```bash -./scripts/run_tests.sh --cleanup -# or -./scripts/run_tests.sh -c +python scripts/check_drift.py --verbose ``` -For more details on testing, see the [tests README](tests/README.md). +### Versioning + +This project follows [Semantic Versioning](https://semver.org/). Breaking +changes only land in major releases and are documented in +[MIGRATION.md](MIGRATION.md) and [CHANGELOG.md](CHANGELOG.md). + +### Contributing -## Contributions +Pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and +guidelines. For bugs and feature requests, open an issue. -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. -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. +## License -### SDK Created by [Speakeasy](https://www.speakeasy.com/?utm_source=youdotcom&utm_campaign=python) +MIT. See [LICENSE](LICENSE). diff --git a/USAGE.md b/USAGE.md index 37e0e9d..b6f6c53 100644 --- a/USAGE.md +++ b/USAGE.md @@ -6,10 +6,10 @@ 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_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=[ @@ -34,10 +34,10 @@ 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_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/searchrequestbody.md b/docs/models/searchrequestbody.md index 82a0428..5bed773 100644 --- a/docs/models/searchrequestbody.md +++ b/docs/models/searchrequestbody.md @@ -5,7 +5,7 @@ | Field | Type | Required | Description | Example | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | What are the latest geopolitical updates from India | +| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://you.com/docs/guides/search-operators) to refine your search. | What are the latest geopolitical updates from India | | `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | | `freshness` | [Optional[models.FreshnessValue]](../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | | `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | 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..6b7d022 --- /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..6004310 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 @@ -19,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=[ @@ -42,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=[ @@ -65,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=[ @@ -88,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=[ @@ -111,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=[ @@ -134,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=[ @@ -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..c23f7ee 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 @@ -10,18 +17,16 @@ 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 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 58c8b5a..a8fe820 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,31 +16,48 @@ 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 with You( - api_key_auth=os.getenv("YDC_API_KEY", ""), + 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,17 +71,17 @@ with You( ``` ### Example Usage: authorizationFailure - + ```python 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.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,17 +95,17 @@ with You( ``` ### Example Usage: invalidOrExpired - + ```python 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.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,17 +119,17 @@ with You( ``` ### Example Usage: invalidParams - + ```python 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.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,17 +143,17 @@ with You( ``` ### Example Usage: missingApiKey - + ```python 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.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,17 +167,17 @@ with You( ``` ### Example Usage: missingScopes - + ```python 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.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,17 +191,17 @@ with You( ``` ### Example Usage: otherAuthParsing - + ```python 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.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=[ @@ -201,7 +218,7 @@ with You( | Parameter | Type | Required | Description | Example | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search. | What are the latest geopolitical updates from India | +| `query` | *str* | :heavy_check_mark: | The search query used to retrieve relevant results from the web. You can also include [search operators](https://you.com/docs/guides/search-operators) to refine your search. | What are the latest geopolitical updates from India | | `count` | *Optional[int]* | :heavy_minus_sign: | Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them). | | | `freshness` | [Optional[models.FreshnessValue]](../../models/freshnessvalue.md) | :heavy_minus_sign: | Specifies the freshness of the results to return. Provide either one of `day`, `week`, `month`, `year`, or a date range string in the format `YYYY-MM-DDtoYYYY-MM-DD`.

When your search query includes a temporal keyword and you also set a freshness parameter, the search will use the broader (i.e., less restrictive) of the two timeframes. For example, if you use `query=news+this+week&freshness=month`, the results will use a freshness of month. | | | `offset` | *Optional[int]* | :heavy_minus_sign: | Indicates the `offset` for pagination. The `offset` is calculated in multiples of `count`. For example, if `count = 5` and `offset = 1`, results 5–10 will be returned. Range `0 ≤ offset ≤ 9`. | | @@ -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. @@ -244,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) @@ -262,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) @@ -280,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) @@ -298,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) @@ -316,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) @@ -334,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) @@ -352,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) @@ -370,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) @@ -388,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) @@ -406,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) @@ -453,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") @@ -497,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) @@ -545,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) @@ -563,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) @@ -581,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) @@ -599,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) @@ -617,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) @@ -635,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) @@ -653,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) @@ -671,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) @@ -689,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) @@ -707,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) @@ -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..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. """ @@ -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) @@ -444,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}") @@ -487,10 +319,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 +341,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 +356,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..33a75fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "youdotcom" -version = "2.5.0" +version = "3.0.0" description = "The official You.com Python SDK." authors = [{ name = "You.com" },] readme = "README.md" @@ -10,15 +10,19 @@ dependencies = [ "httpx >=0.28.1", "pydantic >=2.11.2", ] -license = { text = "Apache-2.0" } +license = "MIT" +license-files = ["LICENSE"] [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 ==1.15.0", - "pylint ==3.2.3", - "pyright ==1.1.398", - "pytest >=8.0.0", - "pytest-asyncio >=0.24.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", + "pytest-cov >=6.0.0, <8", ] [tool.setuptools.packages.find] @@ -34,6 +38,13 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" pythonpath = ["src"] +# The suite closes every client it opens; keep it that way. A leaked transport +# surfaces during GC, which pytest reports as an unraisable exception rather +# than a test failure, so both have to be errors for this to actually enforce. +filterwarnings = [ + "error::ResourceWarning", + "error::pytest.PytestUnraisableExceptionWarning", +] markers = [ "slow: marks tests as slow (DEEP/EXHAUSTIVE research); skip with '-m \"not slow\"'", ] diff --git a/scripts/check_drift.py b/scripts/check_drift.py new file mode 100644 index 0000000..d15baa2 --- /dev/null +++ b/scripts/check_drift.py @@ -0,0 +1,514 @@ +#!/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) + 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) + 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 + +# --------------------------------------------------------------------------- +# 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"), +] + +# 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 +# --------------------------------------------------------------------------- + +def fetch_specs() -> dict[str, dict[str, Any]]: + """Fetch all OpenAPI specs from the You.com docs index.""" + 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 + 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 + + +# --------------------------------------------------------------------------- +# 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 + + +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.removeprefix("#/").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. + + 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: + 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 + + spec_params = _get_schema_properties(schema, spec) + 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 +# --------------------------------------------------------------------------- + +# 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 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() + + print("Fetching OpenAPI specs from you.com...") + 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 + + print(f"Found {len(specs)} specs: {', '.join(sorted(specs.keys()))}") + print() + + 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") + for w in all_warnings: + print(f" ⚠️ {w}") + print() + print("Review the above and update the SDK if needed.") + print("RESULT: drift") + 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__": + sys.exit(main()) 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..9e8656c --- /dev/null +++ b/src/youdotcom/_shims.py @@ -0,0 +1,258 @@ +"""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. + + 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: + 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: OptionalNullable[str] = UNSET, + 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: OptionalNullable[str] = UNSET, + 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: OptionalNullable[str] = UNSET, + 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( + 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( + 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..bbf7cf6 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 = "3.0.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..d91eb22 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 @@ -225,6 +236,9 @@ def _build_request_with_client( timeout = timeout_ms / 1000 if timeout_ms is not None else None + if client is None: + raise ValueError("client is required") + return client.build_request( method, url, @@ -259,7 +273,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 +295,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 +347,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 +372,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..26c3325 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 @@ -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/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..cf51042 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 @@ -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.""" 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..7a95bbf 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 @@ -15,7 +15,7 @@ class SearchRequestBodyTypedDict(TypedDict): query: str - r"""The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search.""" + r"""The search query used to retrieve relevant results from the web. You can also include [search operators](https://you.com/docs/guides/search-operators) to refine your search.""" count: NotRequired[int] r"""Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them).""" freshness: NotRequired[FreshnessValueTypedDict] @@ -53,7 +53,7 @@ class SearchRequestBodyTypedDict(TypedDict): class SearchRequestBody(BaseModel): query: str - r"""The search query used to retrieve relevant results from the web. You can also include [search operators](https://docs.you.com/search/search-operators) to refine your search.""" + r"""The search query used to retrieve relevant results from the web. You can also include [search operators](https://you.com/docs/guides/search-operators) to refine your search.""" count: Optional[int] = 10 r"""Specifies the maximum number of search results to return per section (the sections are `web` and `news`. See the JSON response to visualize them).""" 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..382082c 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 @@ -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/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..d0df774 100644 --- a/src/youdotcom/sdk.py +++ b/src/youdotcom/sdk.py @@ -1,13 +1,12 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from .basesdk import BaseSDK from .httpclient import AsyncHttpClient, ClientOwner, HttpClient, close_clients from .sdkconfiguration import SDKConfiguration from .utils.logger import Logger, get_default_logger from .utils.retries import RetryConfig +import asyncio import httpx -import importlib -import sys from typing import ( Any, Callable, @@ -16,46 +15,70 @@ 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 + +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 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[ @@ -74,7 +97,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 @@ -103,13 +126,33 @@ 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): - # pylint: disable=unnecessary-lambda-assignment - security = lambda: models.Security(api_key_auth=api_key_auth()) - else: + # 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): + + 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) + else: + security = None if server_url is not None: if url_params is not None: @@ -149,43 +192,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 @@ -193,70 +205,630 @@ 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 + if client is not None and not self.sdk_configuration.client_supplied: + try: + client.close() + 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() + 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 _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 + 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 - def search_post( + def __exit__(self, exc_type, exc_val, exc_tb): + self._close_sync_client() + self._close_async_client_from_sync() + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self._close_async_client() + self._close_sync_client() + + 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=_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]]), + ) + 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=_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]]), + ) + 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 - 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 - :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`. + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(models.CONTENTS_OP_SERVERS[0], None) - 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. + 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, + ) - 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. + 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, + ) - 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. + 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( + 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 = 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_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) + + def _search_impl( + self, + *, + query: str, + count: Optional[int] = 10, + freshness: Optional[str] = None, + offset: Optional[int] = None, + country: Optional[str] = None, + language: OptionalNullable[str] = UNSET, + 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``, ``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). + :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. 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. + :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,25 +842,30 @@ 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, + freshness=_lower(freshness), offset=offset, - country=country, - language=language, - 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, ) + # 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( method="POST", @@ -322,12 +899,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 +944,18 @@ def search_post( raise errors.YouDefaultError("Unexpected response received", http_res) - async def search_post_async( + async def search_async( 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: OptionalNullable[str] = UNSET, + 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 +965,11 @@ 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 ``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 @@ -428,25 +979,30 @@ 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, + freshness=_lower(freshness), offset=offset, - country=country, - language=language, - 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, ) + # 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( method="POST", @@ -480,12 +1036,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, @@ -1316,7 +1872,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 @@ -1441,7 +1996,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 diff --git a/src/youdotcom/sdkconfiguration.py b/src/youdotcom/sdkconfiguration.py index ea49a89..049fba3 100644 --- a/src/youdotcom/sdkconfiguration.py +++ b/src/youdotcom/sdkconfiguration.py @@ -1,15 +1,13 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + from ._version import ( - __gen_version__, __openapi_doc_version__, __user_agent__, __version__, ) 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,9 +32,8 @@ 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) + 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/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..1cb98b8 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, @@ -88,7 +88,7 @@ def _populate_query_params( ) for key, value in serialized_parms.items(): if key in query_param_values: - query_param_values[key].extend(value) + query_param_values[key].append(value) else: query_param_values[key] = [value] else: 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..1e76ae1 100644 --- a/src/youdotcom/utils/url.py +++ b/src/youdotcom/utils/url.py @@ -1,4 +1,6 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + + +from urllib.parse import quote from decimal import Decimal from typing import ( @@ -41,7 +43,7 @@ def generate_url( _populate_path_params(gbls, None, path_param_values, globals_already_populated) for key, value in path_param_values.items(): - path = path.replace("{" + key + "}", value, 1) + path = path.replace("{" + key + "}", quote(value, safe=""), 1) return remove_suffix(server_url, "/") + path @@ -144,7 +146,7 @@ def is_optional(field): def template_url(url_with_params: str, params: Dict[str, str]) -> str: for key, value in params.items(): - url_with_params = url_with_params.replace("{" + key + "}", value) + url_with_params = url_with_params.replace("{" + key + "}", quote(value, safe="")) return url_with_params diff --git a/src/youdotcom/utils/values.py b/src/youdotcom/utils/values.py index dae01a4..3be1346 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 @@ -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 diff --git a/tests/PERFORMANCE_TESTING.md b/tests/PERFORMANCE_TESTING.md index e91d623..4823b38 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%) @@ -263,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 ``` @@ -334,11 +330,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..f4e6146 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 @@ -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** (23 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: @@ -163,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 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/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6d8a4d1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +"""Shared pytest configuration for the SDK test suite.""" + +import gc + +import pytest + +from tests.test_client import close_test_clients + + +@pytest.fixture(autouse=True) +def _close_test_clients(): + """Close every HTTP client a test built through the test factories. + + `You` never closes caller-supplied transports — that is deliberate, since + the caller owns anything it passes in — which means each test owns the + clients it hands to the SDK. Doing that centrally keeps the obligation + from being forgotten at every call site, and lets the suite run clean + under ``-W error::ResourceWarning``. + + Clients built with ``httpx.MockTransport`` have no connection pool and so + cannot leak sockets; they do not need to be registered. + + The ``gc.collect()`` is what gives the ``filterwarnings`` guard in + pyproject.toml its teeth. A leaked transport only emits its + ResourceWarning when the object is finalized, which otherwise happens at + an arbitrary later point — often after the session, where it is reported + against nothing and fails no test. Collecting here forces the warning to + surface inside the test that caused it. + """ + yield + close_test_clients() + gc.collect() diff --git a/tests/mockserver/README.md b/tests/mockserver/README.md index 972b31c..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/pathpostv1agentsruns.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`) 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..db777e5 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,9 +11,9 @@ 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/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/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/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/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..2cec3b4 --- /dev/null +++ b/tests/test_answer.py @@ -0,0 +1,386 @@ +"""Tests for youdotcom.answer — POST /v1/answer.""" + +import json +import os +from contextlib import asynccontextmanager, contextmanager + +import httpx +import pytest + +from tests.test_client import create_test_http_client +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 + + +@contextmanager +def _sync_you(handler, *, api_key: str | None = "test-key"): + client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + kwargs: dict = {"server_url": "http://mock.local", "client": client} + if api_key is not None: + kwargs["api_key_auth"] = api_key + yield You(**kwargs) + finally: + client.close() + + +@asynccontextmanager +async def _async_you(handler, *, api_key: str | None = "test-key"): + async_client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + try: + kwargs: dict = {"server_url": "http://mock.local", "async_client": async_client} + if api_key is not None: + kwargs["api_key_auth"] = api_key + yield You(**kwargs) + finally: + await async_client.aclose() + + +class TestAnswerSuccess: + def test_returns_answer_response(self): + with _sync_you(_make_handler(200)) as you: + res = you.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 + ) + + with _sync_you(handler) as you: + you.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 + ) + + with _sync_you(handler) as you: + you.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 + ) + + with _sync_you(handler) as you: + you.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 + ) + + client = httpx.Client(transport=httpx.MockTransport(handler)) + you = You( + server_url="http://custom.local", + client=client, + api_key_auth="test-key", + ) + you.answer(query="test") + assert "http://custom.local" in captured["url"] + assert "/v1/answer" in captured["url"] + client.close() + + 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 + ) + + with _sync_you(handler) as you: + you.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 + ) + + with _sync_you(handler) as you: + you.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): + async with _async_you(_make_handler(200)) as you: + res = await you.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: + with _sync_you(_make_handler(402, body)) as you: + you.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: + with _sync_you(_make_handler(402, body)) as you: + you.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): + with _sync_you(_make_handler(401, body), api_key="bad-key") as you: + you.answer(query="test") + + def test_403_raises_forbidden_error(self): + body = json.dumps({"detail": "Missing required scopes"}) + with pytest.raises(ForbiddenResponseError): + with _sync_you(_make_handler(403, body)) as you: + you.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: + with _sync_you(_make_handler(422, body)) as you: + you.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: + with _sync_you(_make_handler(422, body)) as you: + you.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: + with _sync_you(_make_handler(422, body)) as you: + you.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: + with _sync_you(_make_handler(500, body)) as you: + you.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): + with _sync_you(_make_handler(429, body)) as you: + you.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: + async with _async_you(_make_handler(402, body)) as you: + await you.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): + async with _async_you(_make_handler(500, body)) as you: + await you.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 + client.close() + + 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 + client.close() + + 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 + client.close() + + @pytest.mark.asyncio + async def test_async_answer(self, server_url, api_key): + """Test async answer against mock server.""" + 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() diff --git a/tests/test_client.py b/tests/test_client.py index 229e12c..2dbc984 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,10 +4,43 @@ import uuid +# A client passed to `You(...)` is caller-supplied, and the SDK deliberately +# never closes those — so the test owns its lifetime. Rather than repeat a +# try/finally at every call site, clients built here are registered and closed +# after the test by the autouse `_close_test_clients` fixture in conftest.py. +_OPEN_CLIENTS: list[httpx.Client] = [] + + +def register_test_client(client: httpx.Client) -> httpx.Client: + """Track a client so the autouse fixture in conftest.py will close it.""" + _OPEN_CLIENTS.append(client) + return client + + +def close_test_clients() -> None: + """Close and forget every registered client. Called from conftest.py. + + Closing an already-closed httpx client is a no-op, so this is safe for + tests that also close explicitly. + """ + while _OPEN_CLIENTS: + client = _OPEN_CLIENTS.pop() + try: + client.close() + except Exception: # pragma: no cover - teardown must not fail a test + pass + + 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()), - } + """Create a test HTTP client with tracking headers. + + The client is registered for teardown, so callers do not need to close it. + """ + return register_test_client( + httpx.Client( + headers={ + "x-test-name": test_name, + "x-test-instance-id": str(uuid.uuid4()), + } + ) ) diff --git a/tests/test_client_lifecycle.py b/tests/test_client_lifecycle.py new file mode 100644 index 0000000..db62dd2 --- /dev/null +++ b/tests/test_client_lifecycle.py @@ -0,0 +1,163 @@ +"""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 + + +def _mock_transport() -> httpx.MockTransport: + """A transport with no connection pool. + + The `_Boom*` clients below raise on close by design, so they can never be + disposed of normally. Backing every client here with a mock transport means + there is no pool to leak when that happens. + """ + return httpx.MockTransport(lambda request: httpx.Response(200, json={})) + + +class _RecordingClient(httpx.Client): + closed = False + + def __init__(self): + super().__init__(transport=_mock_transport()) + + def close(self): + type(self).closed = True + super().close() + + +class _RecordingAsyncClient(httpx.AsyncClient): + closed = False + + def __init__(self): + super().__init__(transport=_mock_transport()) + + async def aclose(self): + type(self).closed = True + await super().aclose() + + +class _BoomClient(httpx.Client): + def __init__(self): + super().__init__(transport=_mock_transport()) + + def close(self): + raise RuntimeError("sync close boom") + + +class _BoomAsyncClient(httpx.AsyncClient): + def __init__(self): + super().__init__(transport=_mock_transport()) + + async def aclose(self): + raise RuntimeError("async close boom") + + +def _client() -> httpx.Client: + return httpx.Client(transport=_mock_transport()) + + +def _async_client() -> httpx.AsyncClient: + return httpx.AsyncClient(transport=_mock_transport()) + + +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=_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=_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=_async_client())) 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=_async_client())): + pass 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..00eef1e --- /dev/null +++ b/tests/test_direct_methods.py @@ -0,0 +1,164 @@ +"""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 +from contextlib import asynccontextmanager, contextmanager + +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

"}] +) + + +@contextmanager +def _sync_you(handler, *, api_key: str | None = "test-key"): + client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + kwargs: dict = {"server_url": "http://mock.local", "client": client} + if api_key is not None: + kwargs["api_key_auth"] = api_key + yield You(**kwargs) + finally: + client.close() + + +@asynccontextmanager +async def _async_you(handler, *, api_key: str | None = "test-key"): + async_client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + try: + kwargs: dict = {"server_url": "http://mock.local", "async_client": async_client} + if api_key is not None: + kwargs["api_key_auth"] = api_key + yield You(**kwargs) + finally: + await async_client.aclose() + + +# --------------------------------------------------------------------------- +# you.search() — POST /v1/search +# --------------------------------------------------------------------------- + + +class TestSearchDirect: + def test_returns_search_response(self): + with _sync_you(lambda req: httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + )) as you: + res = you.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 + ) + + with _sync_you(handler) as you: + you.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 + ) + + with _sync_you(handler) as you: + you.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): + async with _async_you(lambda req: httpx.Response( + 200, headers={"content-type": "application/json"}, content=_SEARCH_BODY + )) as you: + res = await you.search_async(query="python") + assert isinstance(res, SearchResponse) + + +# --------------------------------------------------------------------------- +# you.contents() — POST /v1/contents +# --------------------------------------------------------------------------- + + +class TestContentsDirect: + def test_returns_contents_response_list(self): + with _sync_you(lambda req: httpx.Response( + 200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY + )) as you: + res = you.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 + ) + + with _sync_you(handler) as you: + you.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 + ) + + with _sync_you(handler) as you: + you.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): + async with _async_you(lambda req: httpx.Response( + 200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY + )) as you: + res = await you.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..9602415 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,10 +284,11 @@ 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). - Smoke-tests prod to ensure the overlay-generated + Smoke-tests prod to ensure the `Content = Union[str, Dict[str, Any]]` round-trips structured payloads. """ @@ -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 3.0.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_param_normalization.py b/tests/test_param_normalization.py new file mode 100644 index 0000000..945ae2a --- /dev/null +++ b/tests/test_param_normalization.py @@ -0,0 +1,169 @@ +"""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 +from contextlib import contextmanager + +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": []}}) + + +@contextmanager +def _capture(response_body: str): + """Yield (You, captured) over a mock transport, closing the client after. + + Caller-supplied transports are never closed by the SDK, so ownership of + the client stays here. + """ + captured: dict = {} + + def handler(request): + captured["body"] = json.loads(request.content) + return httpx.Response( + 200, headers={"content-type": "application/json"}, content=response_body + ) + + client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + with You( + api_key_auth="k", server_url="http://mock.local", client=client + ) as you: + yield you, captured + finally: + client.close() + + +def _search_body(**kwargs) -> dict: + """Run one search and return the JSON body that went over the wire.""" + with _capture(_SEARCH_BODY) as (you, captured): + you.search(query="x", **kwargs) + return captured["body"] + + +def _answer_body(**kwargs) -> dict: + """Run one answer call and return the JSON body that went over the wire.""" + with _capture(_ANSWER_BODY) as (you, captured): + 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 httpx.AsyncClient(transport=httpx.MockTransport(handler)) as ac: + async with You( + api_key_auth="k", server_url="http://mock.local", async_client=ac + ) 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): + with _capture(_SEARCH_BODY) as (you, captured): + 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): + with _capture(_SEARCH_BODY) as (you, captured): + with pytest.warns(DeprecationWarning): + you.search.unified(query="x", language=None) + + assert "language" not in captured["body"] diff --git a/tests/test_performance.py b/tests/test_performance.py index 5e958eb..5e0166d 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: @@ -29,12 +28,10 @@ print_detailed_metrics, print_metrics_table, ) +from tests.test_client import register_test_client from tests.timing_client import SDKCallTiming, TimingHTTPClient from youdotcom import You from youdotcom.models import ( - ComputeTool, - ResearchTool, - WebSearchTool, Country, ContentsFormats, Freshness, @@ -42,10 +39,6 @@ LiveCrawl, LiveCrawlFormats, SafeSearch, - SearchEffort, - ReportVerbosity, - ExpressAgentRunsRequest, - AdvancedAgentRunsRequest, ) @@ -99,14 +92,20 @@ def show_detailed(): def create_timing_client(test_name: str) -> TimingHTTPClient: - """Create a TimingHTTPClient with test headers.""" - return TimingHTTPClient( + """Create a TimingHTTPClient with test headers. + + Registered for teardown like the other test clients — see + ``tests/test_client.py``. + """ + client = 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()), } ) + register_test_client(client) + return client def measure_sdk_call(func, timing_client: TimingHTTPClient, iterations: int, endpoint_name: str) -> PerformanceMetrics: @@ -168,11 +167,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 +180,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 +193,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 +206,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 +219,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 +232,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 +245,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 +258,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 +271,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 +284,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 +297,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 +310,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 +323,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 +341,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 +359,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 +377,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 +396,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 +415,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 +437,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 +458,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 +477,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 +495,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 +508,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 +525,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 +542,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 +559,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 +576,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 +594,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 +615,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 +636,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_redaction.py b/tests/test_redaction.py new file mode 100644 index 0000000..76c34c7 --- /dev/null +++ b/tests/test_redaction.py @@ -0,0 +1,118 @@ +"""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, + ) + + # Caller-supplied, so this helper owns closing it. + client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + with You( + api_key_auth=API_KEY, + server_url="http://mock.local", + client=client, + debug_logger=logger, + **client_kwargs, + ) as you: + you.search(query="x") + finally: + client.close() + + 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_research.py b/tests/test_research.py index 84a4919..79cea1f 100644 --- a/tests/test_research.py +++ b/tests/test_research.py @@ -132,25 +132,28 @@ def test_research_with_sources(self, server_url, api_key): class TestResearchAsync: @pytest.mark.asyncio async def test_basic_research_async(self, server_url, api_key): - async_client = httpx.AsyncClient( + # Caller-supplied transports are never closed by the SDK, so this test + # owns the client's lifetime. + async with 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, - ) + ) as async_client: + async with You( + server_url=server_url, async_client=async_client, api_key_auth=api_key + ) as you: + res = await you.research_async( + input="What are the latest advances in quantum computing?", + research_effort=ResearchEffort.STANDARD, + server_url=server_url, + ) - async with You(server_url=server_url, async_client=async_client, api_key_auth=api_key) as you: - res = await you.research_async( - input="What are the latest advances in quantum computing?", - research_effort=ResearchEffort.STANDARD, - server_url=server_url, - ) - - assert isinstance(res, ResearchResponse) - assert res.output is not None - assert res.output.content is not None - assert len(res.output.content) > 0 + assert isinstance(res, ResearchResponse) + assert res.output is not None + assert res.output.content is not None + assert len(res.output.content) > 0 class TestResearchErrors: @@ -216,20 +219,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") @@ -304,20 +293,22 @@ def test_research_background_false_returns_research_response(self, server_url, a @pytest.mark.asyncio async def test_get_research_task_async_returns_task_detail(self, server_url, api_key): - async_client = httpx.AsyncClient( + async with 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, - ) - async with You(server_url=server_url, async_client=async_client, api_key_auth=api_key) as you: - detail = await you.get_research_task_async( - task_id="00000000-0000-0000-0000-000000000001", - server_url=server_url, - ) - assert detail.id == "00000000-0000-0000-0000-000000000001" - assert detail.status.value == "completed" + ) as async_client: + async with You( + server_url=server_url, async_client=async_client, api_key_auth=api_key + ) as you: + detail = await you.get_research_task_async( + task_id="00000000-0000-0000-0000-000000000001", + server_url=server_url, + ) + assert detail.id == "00000000-0000-0000-0000-000000000001" + assert detail.status.value == "completed" class TestResearchBackgroundErrors: @@ -403,8 +394,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): @@ -417,8 +407,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( @@ -458,9 +448,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_research_helpers.py b/tests/test_research_helpers.py index 9a9a7e1..9fc3b8f 100644 --- a/tests/test_research_helpers.py +++ b/tests/test_research_helpers.py @@ -5,6 +5,7 @@ import os import time import uuid +from contextlib import asynccontextmanager, contextmanager import httpx import pytest @@ -173,23 +174,24 @@ def test_research_background_returns_task_response(self, server_url, api_key): @pytest.mark.asyncio async def test_research_background_async_returns_task_response(self, server_url, api_key): - async_client = httpx.AsyncClient( + # Caller-supplied transports are never closed by the SDK, so this test + # owns the client's lifetime. + async with 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, - ) - - async with You( - server_url=server_url, async_client=async_client, api_key_auth=api_key - ) as you: - res = await research_background_async( - you, - input="Compare NVIDIA, AMD, and Intel revenue over 5 years", - research_effort=ResearchEffort.DEEP, - server_url=server_url, - ) + ) as async_client: + async with You( + server_url=server_url, async_client=async_client, api_key_auth=api_key + ) as you: + res = await research_background_async( + you, + input="Compare NVIDIA, AMD, and Intel revenue over 5 years", + research_effort=ResearchEffort.DEEP, + server_url=server_url, + ) assert isinstance(res, TaskResponse) assert res.task_id == "00000000-0000-0000-0000-000000000001" @@ -233,6 +235,7 @@ def handler(request): input="What is the capital of France?", research_effort=ResearchEffort.STANDARD, ) + sdk_client.close() @pytest.mark.asyncio async def test_research_background_async_raises_type_error_on_sync_response(self): @@ -266,6 +269,7 @@ def handler(request): input="What is the capital of France?", research_effort=ResearchEffort.STANDARD, ) + await sdk_async_client.aclose() # --------------------------------------------------------------------------- @@ -290,23 +294,22 @@ def test_poll_returns_completed_task_detail(self, server_url, api_key): @pytest.mark.asyncio async def test_poll_async_returns_completed_task_detail(self, server_url, api_key): - async_client = httpx.AsyncClient( + async with 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, - ) - - async with You( - server_url=server_url, async_client=async_client, api_key_auth=api_key - ) as you: - detail = await poll_research_task_async( - you, - "00000000-0000-0000-0000-000000000001", - interval_s=0.01, - timeout_s=2.0, - ) + ) as async_client: + async with You( + server_url=server_url, async_client=async_client, api_key_auth=api_key + ) as you: + detail = await poll_research_task_async( + you, + "00000000-0000-0000-0000-000000000001", + interval_s=0.01, + timeout_s=2.0, + ) assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" @@ -342,6 +345,7 @@ def test_research_and_wait_returns_completed_detail(self): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + you.sdk_configuration.client.close() def test_research_and_wait_error_event_raises_runtime_error(self): """research_and_wait raises RuntimeError when the stream emits an @@ -368,6 +372,7 @@ def test_research_and_wait_error_event_raises_runtime_error(self): input="test query", research_effort=ResearchEffort.STANDARD, ) + you.sdk_configuration.client.close() def test_research_and_wait_ok_event_but_get_non_completed_raises(self): """When the stream emits a terminal OK event (response.done) but the @@ -395,6 +400,7 @@ def test_research_and_wait_ok_event_but_get_non_completed_raises(self): input="test query", research_effort=ResearchEffort.STANDARD, ) + you.sdk_configuration.client.close() def test_research_and_wait_ok_event_but_get_failed_raises_immediately(self): """When the stream emits a terminal OK event but the follow-up GET @@ -422,6 +428,7 @@ def test_research_and_wait_ok_event_but_get_failed_raises_immediately(self): input="test query", research_effort=ResearchEffort.STANDARD, ) + you.sdk_configuration.client.close() def test_research_and_wait_ok_event_repoll_succeeds(self): """When the stream emits a terminal OK event and the first GET returns @@ -472,6 +479,7 @@ def handler(request): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" assert call_count["get"] == 2 # first running, second completed + you.sdk_configuration.client.close() def test_research_and_wait_timeout_raises_timeout_error(self): """research_and_wait raises TimeoutError when the stream never sends @@ -496,6 +504,7 @@ def test_research_and_wait_timeout_raises_timeout_error(self): input="test query", research_effort=ResearchEffort.STANDARD, ) + you.sdk_configuration.client.close() def test_research_and_wait_timeout_falls_back_to_get(self): """When the stream times out (ReadTimeout) but the task has completed, @@ -520,6 +529,7 @@ def test_research_and_wait_timeout_falls_back_to_get(self): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + you.sdk_configuration.client.close() def test_research_and_wait_stream_close_falls_back_to_get(self): """When the stream closes without a terminal event, research_and_wait @@ -544,6 +554,7 @@ def test_research_and_wait_stream_close_falls_back_to_get(self): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + you.sdk_configuration.client.close() def test_research_and_wait_stream_close_task_running_raises_timeout(self): """When the stream closes without a terminal event and the final GET @@ -567,6 +578,7 @@ def test_research_and_wait_stream_close_task_running_raises_timeout(self): input="test query", research_effort=ResearchEffort.STANDARD, ) + you.sdk_configuration.client.close() def test_research_and_wait_total_deadline_not_stall_timeout(self): """research_and_wait enforces a total wall-clock deadline, not just a @@ -600,6 +612,7 @@ def __iter__(self): input="test query", research_effort=ResearchEffort.STANDARD, ) + you.sdk_configuration.client.close() def test_stream_open_transport_error_falls_back_to_poll(self): """When _open_raw_stream raises a TransportError (can't reach server), @@ -635,6 +648,7 @@ def handler(request): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + you.sdk_configuration.client.close() def test_stream_open_401_propagates_typed_error(self): """When _open_raw_stream gets a 401, the typed @@ -674,6 +688,7 @@ def handler(request): input="test query", research_effort=ResearchEffort.STANDARD, ) + you.sdk_configuration.client.close() def test_mid_stream_transport_error_falls_back_to_poll(self): """When a TransportError occurs mid-stream (dropped connection), @@ -716,6 +731,7 @@ def handler(request): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + you.sdk_configuration.client.close() # --------------------------------------------------------------------------- @@ -731,9 +747,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 +772,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,8 +795,9 @@ 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}" + sdk_client.close() # --------------------------------------------------------------------------- # _resolve_default_timeout: auto-adjust timeout based on research_effort. # --------------------------------------------------------------------------- @@ -838,6 +855,7 @@ def test_frontier_auto_timeout_completes_without_premature_timeout(self): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + you.sdk_configuration.client.close() def test_explicit_timeout_overrides_auto_adjust(self): """When the user passes an explicit timeout_s, it takes precedence @@ -861,6 +879,7 @@ def test_explicit_timeout_overrides_auto_adjust(self): input="test query", research_effort=ResearchEffort.FRONTIER, ) + you.sdk_configuration.client.close() # --------------------------------------------------------------------------- @@ -905,80 +924,100 @@ def handler(request): return handler @staticmethod + @contextmanager def _sync_you(handler): - return You( - server_url="http://mock.local", - client=httpx.Client(transport=httpx.MockTransport(handler)), - api_key_auth="test-api-key", - ) + client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + yield You( + server_url="http://mock.local", + client=client, + api_key_auth="test-api-key", + ) + finally: + client.close() @staticmethod - def _async_you(handler): - return You( - server_url="http://mock.local", - async_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), - api_key_auth="test-api-key", - ) + @asynccontextmanager + async def _async_you(handler): + async_client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + try: + yield You( + server_url="http://mock.local", + async_client=async_client, + api_key_auth="test-api-key", + ) + finally: + await async_client.aclose() _TASK = "00000000-0000-0000-0000-000000000001" def test_401_raises_unauthorized_error(self): from youdotcom.errors import StreamResearchTaskUnauthorizedError with pytest.raises(StreamResearchTaskUnauthorizedError): - list(stream_research(self._sync_you(self._make_error_handler(401)), self._TASK)) + with self._sync_you(self._make_error_handler(401)) as you: + list(stream_research(you, self._TASK)) def test_403_raises_forbidden_error(self): from youdotcom.errors import StreamResearchTaskForbiddenError with pytest.raises(StreamResearchTaskForbiddenError): - list(stream_research(self._sync_you(self._make_error_handler(403)), self._TASK)) + with self._sync_you(self._make_error_handler(403)) as you: + list(stream_research(you, self._TASK)) def test_404_raises_not_found_error(self): from youdotcom.errors import StreamResearchTaskNotFoundError with pytest.raises(StreamResearchTaskNotFoundError): - list(stream_research(self._sync_you(self._make_error_handler(404)), self._TASK)) + with self._sync_you(self._make_error_handler(404)) as you: + list(stream_research(you, self._TASK)) def test_500_raises_internal_server_error(self): from youdotcom.errors import StreamResearchTaskInternalServerError with pytest.raises(StreamResearchTaskInternalServerError): - list(stream_research(self._sync_you(self._make_error_handler(500)), self._TASK)) + with self._sync_you(self._make_error_handler(500)) as you: + list(stream_research(you, self._TASK)) def test_4xx_fallback_raises_default_error(self): from youdotcom.errors import YouDefaultError with pytest.raises(YouDefaultError): - list(stream_research(self._sync_you(self._make_error_handler(400)), self._TASK)) + with self._sync_you(self._make_error_handler(400)) as you: + list(stream_research(you, self._TASK)) def test_5xx_fallback_raises_default_error(self): from youdotcom.errors import YouDefaultError with pytest.raises(YouDefaultError): - list(stream_research(self._sync_you(self._make_error_handler(502)), self._TASK)) + with self._sync_you(self._make_error_handler(502)) as you: + list(stream_research(you, self._TASK)) @pytest.mark.asyncio async def test_async_401_raises_unauthorized_error(self): from youdotcom.errors import StreamResearchTaskUnauthorizedError with pytest.raises(StreamResearchTaskUnauthorizedError): - async for _ in stream_research_async(self._async_you(self._make_error_handler(401)), self._TASK): - pass + async with self._async_you(self._make_error_handler(401)) as you: + async for _ in stream_research_async(you, self._TASK): + pass @pytest.mark.asyncio async def test_async_403_raises_forbidden_error(self): from youdotcom.errors import StreamResearchTaskForbiddenError with pytest.raises(StreamResearchTaskForbiddenError): - async for _ in stream_research_async(self._async_you(self._make_error_handler(403)), self._TASK): - pass + async with self._async_you(self._make_error_handler(403)) as you: + async for _ in stream_research_async(you, self._TASK): + pass @pytest.mark.asyncio async def test_async_404_raises_not_found_error(self): from youdotcom.errors import StreamResearchTaskNotFoundError with pytest.raises(StreamResearchTaskNotFoundError): - async for _ in stream_research_async(self._async_you(self._make_error_handler(404)), self._TASK): - pass + async with self._async_you(self._make_error_handler(404)) as you: + async for _ in stream_research_async(you, self._TASK): + pass @pytest.mark.asyncio async def test_async_500_raises_internal_server_error(self): from youdotcom.errors import StreamResearchTaskInternalServerError with pytest.raises(StreamResearchTaskInternalServerError): - async for _ in stream_research_async(self._async_you(self._make_error_handler(500)), self._TASK): - pass + async with self._async_you(self._make_error_handler(500)) as you: + async for _ in stream_research_async(you, self._TASK): + pass # --------------------------------------------------------------------------- @@ -1019,6 +1058,7 @@ def handler(request): interval_s=0.01, timeout_s=0.05, ) + sdk_client.close() def test_poll_failed_status_raises_runtime_error(self): """poll_research_task must raise RuntimeError when the task ends @@ -1054,6 +1094,7 @@ def handler(request): interval_s=0.01, timeout_s=2.0, ) + sdk_client.close() class TestPollResearchTaskAsyncErrorPaths: @pytest.mark.asyncio @@ -1089,6 +1130,7 @@ def handler(request): interval_s=0.01, timeout_s=0.05, ) + await sdk_async_client.aclose() @pytest.mark.asyncio async def test_poll_async_failed_status_raises_runtime_error(self): @@ -1124,6 +1166,7 @@ def handler(request): interval_s=0.01, timeout_s=2.0, ) + await sdk_async_client.aclose() # --------------------------------------------------------------------------- @@ -1177,6 +1220,7 @@ def record_send(request): assert isinstance(events[1], RawStreamEvent) assert events[1].data == {"query": "markets", "phase": "searching"} assert recorded_ua["value"] == f"youdotcom-python-sdk/{you.sdk_configuration.sdk_version}" + await sdk_async_client.aclose() class TestResearchAndWaitAsync: @@ -1206,6 +1250,7 @@ async def test_async_research_and_wait_returns_completed_detail(self): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_frontier_auto_timeout_completes_without_premature_timeout(self): @@ -1235,6 +1280,7 @@ async def test_async_frontier_auto_timeout_completes_without_premature_timeout(s assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_research_and_wait_error_event_raises_runtime_error(self): @@ -1262,6 +1308,7 @@ async def test_async_research_and_wait_error_event_raises_runtime_error(self): input="test query", research_effort=ResearchEffort.STANDARD, ) + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_research_and_wait_ok_event_repoll_succeeds(self): @@ -1310,6 +1357,7 @@ def handler(request): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" assert call_count["get"] == 2 + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_research_and_wait_ok_event_but_get_failed_raises_immediately(self): @@ -1338,6 +1386,7 @@ async def test_async_research_and_wait_ok_event_but_get_failed_raises_immediatel input="test query", research_effort=ResearchEffort.STANDARD, ) + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_research_and_wait_timeout_raises_timeout_error(self): @@ -1364,6 +1413,7 @@ async def test_async_research_and_wait_timeout_raises_timeout_error(self): input="test query", research_effort=ResearchEffort.STANDARD, ) + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_research_and_wait_httpx_readtimeout_falls_back_to_get(self): @@ -1399,6 +1449,7 @@ async def __aiter__(self): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_research_and_wait_timeout_falls_back_to_get(self): @@ -1425,6 +1476,7 @@ async def test_async_research_and_wait_timeout_falls_back_to_get(self): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_research_and_wait_stream_close_falls_back_to_get(self): @@ -1451,6 +1503,7 @@ async def test_async_research_and_wait_stream_close_falls_back_to_get(self): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_research_and_wait_stream_close_task_running_raises_timeout(self): @@ -1476,6 +1529,7 @@ async def test_async_research_and_wait_stream_close_task_running_raises_timeout( input="test query", research_effort=ResearchEffort.STANDARD, ) + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_stream_open_transport_error_falls_back_to_poll(self): @@ -1510,6 +1564,7 @@ def handler(request): assert isinstance(detail, TaskDetail) assert detail.status.value == "completed" + await you.sdk_configuration.async_client.aclose() @pytest.mark.asyncio async def test_async_stream_open_401_propagates_typed_error(self): @@ -1548,3 +1603,4 @@ def handler(request): input="test query", research_effort=ResearchEffort.STANDARD, ) + await you.sdk_configuration.async_client.aclose() 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_security_env.py b/tests/test_security_env.py index a56cb22..6ba04c9 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 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. + +`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,101 @@ 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 + ) + + # Caller-supplied transports are never closed by the SDK, so close it here. + client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + with You( + server_url="http://mock.local", + client=client, + **client_kwargs, + ) as you: + you.search(query="x") + finally: + client.close() + 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.""" + # A mock transport guarantees the assertion can't depend on a network + # call: the raise has to happen while the request is being built. + client = httpx.Client( + transport=httpx.MockTransport(lambda req: httpx.Response(200, json={})) + ) + try: + with You(api_key_auth=lambda: "", client=client) as you: # construction is fine + with pytest.raises(ValueError, match="callable returned an empty API key"): + you.search(query="x") + finally: + client.close() + + 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/tests/test_shims.py b/tests/test_shims.py new file mode 100644 index 0000000..696876b --- /dev/null +++ b/tests/test_shims.py @@ -0,0 +1,108 @@ +"""Tests for backward-compat sub-SDK shims with DeprecationWarning.""" + +import json +import warnings +from contextlib import contextmanager + +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

"}]) + + +@contextmanager +def _you(handler, api_key="test"): + client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + yield You( + server_url="http://mock.local", + client=client, + api_key_auth=api_key, + ) + finally: + client.close() + + +class TestSearchShim: + def test_direct_call_no_warning(self): + with _you(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_SEARCH_BODY)) as you: + 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): + with _you(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_SEARCH_BODY)) as you: + 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_client = httpx.AsyncClient(transport=httpx.MockTransport(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_SEARCH_BODY))) + try: + async_you = You( + server_url="http://mock.local", + async_client=async_client, + 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) + finally: + await async_client.aclose() + + +class TestContentsShim: + def test_direct_call_no_warning(self): + with _you(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY)) as you: + 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): + with _you(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY)) as you: + 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_client = httpx.AsyncClient(transport=httpx.MockTransport(lambda req: httpx.Response(200, headers={"content-type": "application/json"}, content=_CONTENTS_BODY))) + try: + async_you = You( + server_url="http://mock.local", + async_client=async_client, + 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 + finally: + await async_client.aclose() 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..3ca2536 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]] @@ -69,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" @@ -83,7 +228,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 +299,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 +397,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 +482,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 +653,7 @@ wheels = [ [[package]] name = "pylint" -version = "3.2.3" +version = "4.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, @@ -402,9 +665,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]] @@ -452,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" @@ -533,7 +810,7 @@ wheels = [ [[package]] name = "youdotcom" -version = "2.5.0" +version = "3.0.0" source = { editable = "." } dependencies = [ { name = "httpcore" }, @@ -548,6 +825,7 @@ dev = [ { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov" }, ] [package.metadata] @@ -559,9 +837,10 @@ 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,<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" }, ]